From c225da5f298acb4cb2b39022382543c0c966d428 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 15 Jul 2023 00:33:22 +0200 Subject: [PATCH 01/70] =?UTF-8?q?fix:=20=F0=9F=9A=91=EF=B8=8F=20TF=20by=20?= =?UTF-8?q?default=20fills=20vram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was causing all kind of issues --- __init__.py | 4 ++++ web/color_widget.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 977694d..e53d5a4 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,7 @@ +import os + +os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true" + import traceback from .log import log, blue_text, cyan_text, get_summary, get_label from .utils import here diff --git a/web/color_widget.js b/web/color_widget.js index 4bdca72..c598599 100644 --- a/web/color_widget.js +++ b/web/color_widget.js @@ -29,7 +29,7 @@ function getBrightness(rgbObj) { /** - * @returns {import("/types/litegraph").IWidget} widget + * @returns {import("./types/litegraph").IWidget} widget */ const custom = (key, val, compute = false) => { /** @type {import("/types/litegraph").IWidget} */ From 629e2b5f5fbebe4e79e8b7a4cff2de6017e79225 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 15 Jul 2023 00:39:15 +0200 Subject: [PATCH 02/70] =?UTF-8?q?feat:=20=F0=9F=8E=A8=20add=20support=20fo?= =?UTF-8?q?r=20image.size(0)=20=3D=3D=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attempt at ignoring a branch if the image size is 0. --- nodes/image_interpolation.py | 6 ++++++ utils.py | 22 ++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/nodes/image_interpolation.py b/nodes/image_interpolation.py index 1f94a5a..cfaf337 100644 --- a/nodes/image_interpolation.py +++ b/nodes/image_interpolation.py @@ -82,6 +82,10 @@ class FilmInterpolation: film_model: interpolator.Interpolator, ): n = images.size(0) + # check if images is an empty tensor and return it... + if n == 0: + return (images,) + # check if tensorflow GPU is available available_gpus = tf.config.list_physical_devices("GPU") if not len(available_gpus): @@ -184,6 +188,8 @@ class ExportToProRes: fps: float, prefix: str, ): + if images.size(0) == 0: + return ("",) output_dir = Path(folder_paths.get_output_directory()) id = f"{prefix}_{uuid.uuid4()}.mov" diff --git a/utils.py b/utils.py index ce94de4..7ab1bcc 100644 --- a/utils.py +++ b/utils.py @@ -5,6 +5,7 @@ from pathlib import Path import sys from typing import Union, List +from .log import log def add_path(path, prepend=False): @@ -45,16 +46,21 @@ add_path(comfy_dir) add_path((comfy_dir / "custom_nodes")) -def tensor2pil(image: torch.Tensor) -> Union[Image.Image, List[Image.Image]]: +def tensor2pil(image: torch.Tensor) -> List[Image.Image]: batch_count = 1 if len(image.shape) > 3: batch_count = image.size(0) - if batch_count == 1: - return Image.fromarray( + if batch_count > 1: + out = [] + out.extend([tensor2pil(image[i]) for i in range(batch_count)]) + return out + + return [ + Image.fromarray( np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8) ) - return [tensor2pil(image[i]) for i in range(batch_count)] + ] def pil2tensor(image: Image.Image | List[Image.Image]) -> torch.Tensor: @@ -76,5 +82,9 @@ def tensor2np(tensor: torch.Tensor) -> Union[np.ndarray, List[np.ndarray]]: if len(tensor.shape) > 3: batch_count = tensor.size(0) if batch_count > 1: - return [tensor2np(tensor[i]) for i in range(batch_count)] - return np.clip(255.0 * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8) + out = [] + out.extend([tensor2np(tensor[i]) for i in range(batch_count)]) + return out + + return [np.clip(255.0 * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)] + From 3faadc4b8a5049cb8c264b8a3d50565adec405f1 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 15 Jul 2023 03:04:32 +0200 Subject: [PATCH 03/70] =?UTF-8?q?feat:=20=F0=9F=94=A5=20add=20batch=20supp?= =?UTF-8?q?ort=20for=20load=20image=20sequence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 10 +++ nodes/video.py | 178 ++++++++++++++++++++++++++++++++++++++----------- utils.py | 3 +- 3 files changed, 152 insertions(+), 39 deletions(-) diff --git a/__init__.py b/__init__.py index e53d5a4..6f28319 100644 --- a/__init__.py +++ b/__init__.py @@ -83,3 +83,13 @@ log.info( for k, doc in NODE_CLASS_MAPPINGS_DEBUG.items() ) ) + +# - WAS Dictionary +MANIFEST = { + "name": "MTB Nodes", # The title that will be displayed on Node Class menu,. and Node Class view + "version": (0, 1, 0), # Version of the custom_node or sub module + "author": "Mel Massadian", # Author or organization of the custom_node or sub module + "project": "https://github.com/melMass/comfy_mtb", # The address that the `name` value will link to on Node Class Views + "description": "Set of nodes that enhance your animation workflow and provide a range of useful tools including features such as manipulating bounding boxes, perform color corrections, swap faces in images, interpolate frames for smooth animation, export to ProRes format, apply various image operations, work with latent spaces, generate QR codes, and create normal and height maps for textures.", +} + diff --git a/nodes/video.py b/nodes/video.py index 86b25f9..6e9ae67 100644 --- a/nodes/video.py +++ b/nodes/video.py @@ -10,87 +10,173 @@ from pathlib import Path import json from ..log import log + + class LoadImageSequence: """Load an image sequence from a folder. The current frame is used to determine which image to load. Usually used in conjunction with the `Primitive` node set to increment to load a sequence of images from a folder. + Use -1 to load all matching frames as a batch. """ + @classmethod def INPUT_TYPES(cls): return { "required": { - "path": ("STRING",{"default":"videos/####.png"}), - "current_frame": ("INT",{"default":0, "min":0, "max": 9999999},), + "path": ("STRING", {"default": "videos/####.png"}), + "current_frame": ( + "INT", + {"default": 0, "min": -1, "max": 9999999}, + ), } } CATEGORY = "video" FUNCTION = "load_image" - RETURN_TYPES = ("IMAGE", "MASK", "INT",) - RETURN_NAMES = ("image", "mask", "current_frame",) + RETURN_TYPES = ( + "IMAGE", + "MASK", + "INT", + ) + RETURN_NAMES = ( + "image", + "mask", + "current_frame", + ) def load_image(self, path=None, current_frame=0): + load_all = current_frame == -1 + + if load_all: + log.debug(f"Loading all frames from {path}") + frames = resolve_all_frames(path) + log.debug(f"Found {len(frames)} frames") + + imgs = [] + masks = [] + + for frame in frames: + img, mask = img_from_path(frame) + imgs.append(img) + masks.append(mask) + + out_img = torch.cat(imgs, dim=0) + out_mask = torch.cat(masks, dim=0) + + return ( + out_img, + out_mask, + ) + log.debug(f"Loading image: {path}, {current_frame}") print(f"Loading image: {path}, {current_frame}") resolved_path = resolve_path(path, current_frame) image_path = folder_paths.get_annotated_filepath(resolved_path) - 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") - return (image, mask, current_frame,) + image, mask = img_from_path(image_path) + return ( + image, + mask, + current_frame, + ) @staticmethod def IS_CHANGED(path="", current_frame=0): print(f"Checking if changed: {path}, {current_frame}") resolved_path = resolve_path(path, current_frame) image_path = folder_paths.get_annotated_filepath(resolved_path) - if os.path.exists(image_path): + if os.path.exists(image_path): m = hashlib.sha256() - with open(image_path, 'rb') as f: + with open(image_path, "rb") as f: m.update(f.read()) return m.digest().hex() return "NONE" # @staticmethod # def VALIDATE_INPUTS(path="", current_frame=0): - + # print(f"Validating inputs: {path}, {current_frame}") # resolved_path = resolve_path(path, current_frame) # if not folder_paths.exists_annotated_filepath(resolved_path): # return f"Invalid image file: {resolved_path}" # return True + +import glob + + +def img_from_path(path): + img = Image.open(path) + img = ImageOps.exif_transpose(img) + image = img.convert("RGB") + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + if "A" in img.getbands(): + mask = np.array(img.getchannel("A")).astype(np.float32) / 255.0 + mask = 1.0 - torch.from_numpy(mask) + else: + mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") + return ( + image, + mask, + ) + + +def resolve_all_frames(pattern): + folder_path, file_pattern = os.path.split(pattern) + + log.debug(f"Resolving all frames in {folder_path}") + frames = [] + hash_count = file_pattern.count("#") + frame_pattern = re.sub(r"#+", "*", file_pattern) + + log.debug(f"Found pattern: {frame_pattern}") + + matching_files = glob.glob(os.path.join(folder_path, frame_pattern)) + + log.debug(f"Found {len(matching_files)} matching files") + + frame_regex = re.escape(file_pattern).replace(r"\#", r"(\d+)") + + frame_number_regex = re.compile(frame_regex) + + for file in matching_files: + match = frame_number_regex.search(file) + if match: + frame_number = match.group(1) + log.debug(f"Found frame number: {frame_number}") + # resolved_file = pattern.replace("*" * frame_number.count("#"), frame_number) + frames.append(file) + + frames.sort() # Sort frames alphabetically + return frames + + def resolve_path(path, frame): hashes = path.count("#") padded_number = str(frame).zfill(hashes) return re.sub("#+", padded_number, path) + class SaveImageSequence: """Save an image sequence to a folder. The current frame is used to determine which image to save. This is merely a wrapper around the `save_images` function with formatting for the output folder and filename. """ + def __init__(self): self.output_dir = folder_paths.get_output_directory() self.type = "output" - @classmethod def INPUT_TYPES(cls): - return {"required": { - "images": ("IMAGE", ), - "filename_prefix": ("STRING", {"default": "Sequence"}), - "current_frame": ("INT", {"default": 0, "min": 0, "max": 9999999}), - }, - "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, - } + return { + "required": { + "images": ("IMAGE",), + "filename_prefix": ("STRING", {"default": "Sequence"}), + "current_frame": ("INT", {"default": 0, "min": 0, "max": 9999999}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } RETURN_TYPES = () FUNCTION = "save_images" @@ -99,7 +185,14 @@ class SaveImageSequence: CATEGORY = "image" - def save_images(self, images, filename_prefix="Sequence", current_frame=0, prompt=None, extra_pnginfo=None): + def save_images( + self, + images, + filename_prefix="Sequence", + current_frame=0, + prompt=None, + extra_pnginfo=None, + ): # 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 image in images: @@ -120,30 +213,39 @@ class SaveImageSequence: # "type": self.type # }) # counter += 1 - + if len(images) > 1: raise ValueError("Can only save one image at a time") - + resolved_path = Path(self.output_dir) / filename_prefix resolved_path.mkdir(parents=True, exist_ok=True) - + resolved_img = resolved_path / f"{filename_prefix}_{current_frame:05}.png" - + output_image = images[0].cpu().numpy() - img = Image.fromarray(np.clip(output_image * 255., 0, 255).astype(np.uint8)) + img = Image.fromarray(np.clip(output_image * 255.0, 0, 255).astype(np.uint8)) 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(resolved_img, pnginfo=metadata, compress_level=4) - return { "ui": { "images": [ { "filename": resolved_img.name, "subfolder": resolved_path.name, "type": self.type } ] } } - - - + return { + "ui": { + "images": [ + { + "filename": resolved_img.name, + "subfolder": resolved_path.name, + "type": self.type, + } + ] + } + } + + __nodes__ = [ LoadImageSequence, SaveImageSequence, -] \ No newline at end of file +] diff --git a/utils.py b/utils.py index 7ab1bcc..f90be5b 100644 --- a/utils.py +++ b/utils.py @@ -83,7 +83,8 @@ def tensor2np(tensor: torch.Tensor) -> Union[np.ndarray, List[np.ndarray]]: batch_count = tensor.size(0) if batch_count > 1: out = [] - out.extend([tensor2np(tensor[i]) for i in range(batch_count)]) + for i in range(batch_count): + out.extend(tensor2np(tensor[i])) return out return [np.clip(255.0 * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)] From cd1b603565464fe98a718e1fbaa8c7cd84057576 Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Sun, 16 Jul 2023 17:06:12 +0200 Subject: [PATCH 04/70] =?UTF-8?q?chore:=20=F0=9F=8D=BB=20add=20bmc=20to=20?= =?UTF-8?q?readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5457fbe..fb7506f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## MTB Nodes +Buy Me A Coffee + Welcome to the MTB Nodes project! This codebase is open for you to explore and utilize as you wish. Its primary purpose is to build proof-of-concepts (POCs) for implementation in [MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs). Many nodes in this project are inspired by existing community contributions or built-in functionalities. Before proceeding, please be aware of the licenses associated with certain libraries used in this project. For example, the `deepbump` library is licensed under [GPLv3](https://github.com/HugoTini/DeepBump/blob/master/LICENSE). From f634fe0e6b2db28138e4bd7932fbfc8606a0f033 Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Sun, 16 Jul 2023 18:18:39 +0200 Subject: [PATCH 05/70] =?UTF-8?q?chore:=20=F0=9F=8D=BB=20create=20FUNDING.?= =?UTF-8?q?yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/FUNDING.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..84866f1 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,14 @@ +# These are supported funding model platforms + +github: [melMass] +custom: ["https://www.buymeacoffee.com/melmass"] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry + From da559b9eaf135a49c0ab9bfa45573baf0c18dfb2 Mon Sep 17 00:00:00 2001 From: melMass Date: Sun, 16 Jul 2023 20:49:16 +0200 Subject: [PATCH 06/70] =?UTF-8?q?docs:=20=F0=9F=93=9D=20add=20jp=20and=20c?= =?UTF-8?q?n=20(using=20deep=20translation)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INSTALL-CN.md | 72 +++++++++++++++++++++++++++++++++++ INSTALL-JP.md | 71 +++++++++++++++++++++++++++++++++++ README-CN.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ README-JP.md | 101 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 346 insertions(+) create mode 100644 INSTALL-CN.md create mode 100644 INSTALL-JP.md create mode 100644 README-CN.md create mode 100644 README-JP.md diff --git a/INSTALL-CN.md b/INSTALL-CN.md new file mode 100644 index 0000000..479f46c --- /dev/null +++ b/INSTALL-CN.md @@ -0,0 +1,72 @@ +# 安装 + +- [安装](#安装) + - [依赖项](#依赖项) + - [自定义虚拟环境(我主要使用此方法)](#自定义虚拟环境我主要使用此方法) + - [Comfy-portable / 独立安装(来自 ComfyUI 发布)](#comfy-portable--独立安装来自-comfyui-发布) + - [Google Colab](#google-colab) + - [模型下载](#模型下载) + - [Web 扩展](#web-扩展) + + +### 依赖项 + +#### 自定义虚拟环境(我主要使用此方法) + +1. 确保您处于用于 ComfyUI 的 Python 环境中。 +2. 运行以下命令安装所需的依赖项: + ```bash + pip install -r comfy_mtb/requirements.txt + ``` + +#### Comfy-portable / 独立安装(来自 ComfyUI 发布) + +如果您使用的是 ComfyUI 独立版的 `python-embeded`,则无法使用二进制文件进行依赖项的 pip 安装,特别是当它们没有预编译的 wheels 时。在这种情况下,请查看最新的[发布版](https://github.com/melMass/comfy_mtb/releases),其中包含了为 Linux 和 Windows 构建的预编译 wheels(仅对需要从源代码构建的部分)。有关更多信息,请参阅[此问题(#1)](https://github.com/melMass/comfy_mtb/issues/1)。 +![image](https://github.com/melMass/comfy_mtb/assets/7041726/2934fa14-3725-427c-8b9e-2b4f60ba1b7b) + + +#### Google Colab + +在 **运行 ComfyUI with localtunnel(推荐方式)** 标题下方添加一个新的代码单元格(在代码单元格之前)。 +![在 Colab 中添加位置的预览](https://github.com/melMass/comfy_mtb/assets/7041726/35df2ef1-14f9-44cd-aa65-353829188cd7) + + +```python +# 下载节点 +!git clone --recursive https://github.com/melMass/comfy_mtb.git custom_nodes/comfy_mtb + +# 下载所有模型 +!python custom_nodes/comfy_mtb/scripts/download_models.py -y + +# 安装依赖项 +!pip install -r custom_nodes/comfy_mtb/requirements.txt -f https://download.openmmlab.com/mmcv/dist/cu118/torch2.0/index.html +``` +如果运行后,Colab 抱怨需要重新启动运行时,请重新启动,并且不要重新运行之前的单元格,只需运行一个用于运行 localtunnel 的单元格即可(您可能需要先添加一个 `%cd ComfyUI` 的单元格...)。 + +> **注意**: +> 如果您不需要所有模型,请将 `-y` 删除,因为 Colab 实际上支持用户输入: ![image](https://github.com/melMass/comfy_mtb/assets/7041726/40fc3602-f1d4-432a-98fd-ce2240f5ad06) + +> **预览** +> ![image](https://github.com/melMass/comfy_mtb/assets/7041726/b5b2b2d9-f1e8-4c43-b1db-7dfc5e07be86) + + + +### 模型下载 +某些节点需要下载额外的模型,您可以使用与上述相同的 Python 环境进行交互式下载: +```bash +python scripts/download_models.py +``` + +然后按照提示操作或只需按 Enter 键下载所有模型。 + +> **注意** +> 您可以使用以下命令无需提示下载所有模型: +> ```bash +> python scripts/download_models.py -y +> ``` + +### Web 扩展 + +第一次运行脚本时,会[尝试创建符号链接](https://github.com/melMass/comfy_mtb/blob/d982b69a58c05ccead9c49370764beaa4549992a/__init__.py#L45-L61),将[Web 扩展](https://github.com/melMass/comfy_mtb/tree/main/web)链接到 Comfy 的 `web/extensions` 文件夹。如果失败,您可以手动将 `mtb` 文件夹复制到 `ComfyUI/web/extensions`,目前它只提供了一个颜色小部件,由几个节点共享: + +color widget preview \ No newline at end of file diff --git a/INSTALL-JP.md b/INSTALL-JP.md new file mode 100644 index 0000000..61212ee --- /dev/null +++ b/INSTALL-JP.md @@ -0,0 +1,71 @@ +# インストール +- [インストール](#インストール) + - [依存関係](#依存関係) + - [カスタム仮想環境(主に使用)](#カスタム仮想環境主に使用) + - [Comfyポータブル/スタンドアロン(ComfyUIリリースから)](#comfyポータブルスタンドアロンcomfyuiリリースから) + - [Google Colab](#google-colab) + - [モデルのダウンロード](#モデルのダウンロード) + - [Web拡張機能](#web拡張機能) + + +### 依存関係 + +#### カスタム仮想環境(主に使用) + +1. ComfyUIで使用するPython環境にいることを確認してください。 +2. 次のコマンドを実行して、必要な依存関係をインストールします。 + ```bash + pip install -r comfy_mtb/requirements.txt + ``` +#### Comfyポータブル/スタンドアロン(ComfyUIリリースから) + +ComfyUIスタンドアロンの`python-embeded`を使用している場合、ホイールがない場合にはバイナリの依存関係をpipでインストールすることはできません。この場合は、最新の[リリース](https://github.com/melMass/comfy_mtb/releases)を確認してください。ビルド元のソースが必要な場合は、linuxとwindows用のプリビルドホイールがバンドルされています。詳細については、[この問題(#1)](https://github.com/melMass/comfy_mtb/issues/1)を参照してください。 +![image](https://github.com/melMass/comfy_mtb/assets/7041726/2934fa14-3725-427c-8b9e-2b4f60ba1b7b) + + +#### Google Colab + +**Run ComfyUI with localtunnel (Recommended Way)**ヘッダーのすぐ後ろに新しいコードセルを追加します(コードセルの前) +![colabでの追加方法のプレビュー](https://github.com/melMass/comfy_mtb/assets/7041726/35df2ef1-14f9-44cd-aa65-353829188cd7) + + +```python +# ノードをダウンロード +!git clone --recursive https://github.com/melMass/comfy_mtb.git custom_nodes/comfy_mtb + +# すべてのモデルをダウンロード +!python custom_nodes/comfy_mtb/scripts/download_models.py -y + +# 依存関係をインストール +!pip install -r custom_nodes/comfy_mtb/requirements.txt -f https://download.openmmlab.com/mmcv/dist/cu118/torch2.0/index.html +``` +これを実行した後、colabがランタイムを再起動するようにクレームをつける場合は、再起動してから以前のセルを再実行せずに、ローカルトンネルを実行するセルのみを実行します(最初に`%cd ComfyUI`というセルを追加する必要があるかもしれません...) + + +> **注意**: +> すべてのモデルが必要ない場合は、collabは実際にユーザー入力をサポートしているため、`-y`を削除します: ![image](https://github.com/melMass/comfy_mtb/assets/7041726/40fc3602-f1d4-432a-98fd-ce2240f5ad06) + +> **プレビュー** +> ![image](https://github.com/melMass/comfy_mtb/assets/7041726/b5b2b2d9-f1e8-4c43-b1db-7dfc5e07be86) + + + +### モデルのダウンロード +一部のノードには追加のモデルのダウンロードが必要です。上記と同じPython環境を使用して対話的に行うことができます。 +```bash +python scripts/download_models.py +``` + +その後、プロンプトに従ってモデルをダウンロードするか、単にEnterキーを押してすべてのモデルをダウンロードします。 + +> **注意** +> プロンプトなしですべてのモデルをダウンロードするには、次のコマンドを使用します: + ```bash + python scripts/download_models.py -y + ``` + +### Web拡張機能 + +最初の実行時に、スクリプトは[web拡張機能](https://github.com/melMass/comfy_mtb/tree/main/web)をComfyの`web/extensions`フォルダにシンボリックリンクしようとします。これが失敗する場合は、`mtb`フォルダを手動で`ComfyUI/web/extensions`にコピーしてください。現時点では、いくつかのノードで共有されるカラーウィジェットのみが提供されています。 + +color widget preview \ No newline at end of file diff --git a/README-CN.md b/README-CN.md new file mode 100644 index 0000000..106040c --- /dev/null +++ b/README-CN.md @@ -0,0 +1,102 @@ +## MTB Nodes + +Buy Me A Coffee + +欢迎使用 MTB Nodes 项目!这个代码库是开放的,您可以自由地探索和利用。它的主要目的是构建用于 [MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs) 中的概念验证(POCs)。该项目中的许多节点都是受到现有社区贡献或内置功能的启发而创建的。 + +在继续之前,请注意与此项目中使用的某些库相关的许可证。例如,`deepbump` 库采用 [GPLv3](https://github.com/HugoTini/DeepBump/blob/master/LICENSE) 许可证。 + +- [MTB Nodes](#mtb-nodes) +- [安装](#安装) +- [节点列表](#节点列表) + - [bbox](#bbox) + - [colors](#colors) + - [人脸检测/交换](#人脸检测交换) + - [图像插值(动画)](#图像插值动画) + - [图像操作](#图像操作) + - [潜在变量工具](#潜在变量工具) + - [其他工具](#其他工具) + - [纹理](#纹理) +- [Comfy 资源](#comfy-资源) + + +## 安装 + +- 移至 [INSTALL-CN.md](./INSTALL-CN.md) + + +## 节点列表 + +### bbox +- `Bounding Box`: BBox 构造函数(自定义类型) +- `BBox From Mask`: 从遮罩中提取边界框 +- `Crop`: 根据边界框裁剪图像 +- `Uncrop`: 根据边界框还原图像 + +### colors +- `Colored Image`: 给定尺寸的纯色图像 +- `RGB to HSV`: - +- `HSV to RGB`: - +- `Color Correct`: 基本颜色校正工具 + + +### 人脸检测/交换 +- `Face Swap`: 使用 deepinsight/insightface 模型进行人脸交换(该节点在早期版本中称为 `Roop`,功能相同,`Roop` 只是使用这些模型的应用程序) + > **注意** + > 人脸索引允许您选择要替换的人脸,如下所示: + +- `Load Face Swap Model`: 加载 insightface 模型用于人脸交换 +- `Restore Face`: 使用 [GFPGan](https://github.com/TencentARC/GFPGAN) 还原人脸,与 `Face Swap` 配合使用效果很好,并支持 `bg_upscaler` 的 Comfy 原生放大器 + +### 图像插值(动画) +- `Load Film Model`: 加载 [FILM](https://github.com/google-research/frame-interpolation) 模型 +- `Film Interpolation`: 使用 [FILM](https://github.com/google-research/frame-interpolation) 处理输入帧 + +- `Export to Prores (experimental)`: 将输入帧导出为 ProRes 4444 mov 文件。这使用 ffmpeg stdin 发送原始的 NumPy 数组,与 `Film Interpolation` 一起使用,目前很简单,但可以进一步扩展。 + +### 图像操作 +- `Blur`: 使用高斯滤波器对图像进行模糊处理。 +- `Deglaze Image`: 从 [FN16](https://github.com/Fannovel16/FN16-ComfyUI-nodes/blob/main/DeglazeImage.py) 中提取 +- `Denoise`: 对输入图像进行降噪处理 +- `Image Compare`: 比较两个图像并返回差异图像 +- `Image Premultiply`: 使用掩码对图像进行预乘处理 +- `Image Remove Background Rembg`: 使用 [RemBG](https://github.com/danielgatis/rembg) 进行背景去除 + +- `Image Resize Factor`: 大部分提取自 [WAS Node Suite](https://github.com/WASasquatch/was-node-suite-comfyui),经过一些编辑(特别是支持多个图像)和较少的功能。 +- `Mask To Image`: 将遮罩(Alpha)转换为带有颜色和背景的 RGB 图像 +- `Save Image Grid`: 将输入批次中的所有图像保存为图像网格。 + +### 潜在变量工具 +- `Latent Lerp`: 两个潜在变量之间的线性插值(混合) + + +### 其他工具 +- `Concat Images`: 接受两个图像流,并将它们合并为其他 Comfy 管道支持的图像批次。 +- `Image Resize Factor`: **已弃用**,因为我后来发现了内 + +置的图像调整大小功能。 +- `Text To Image`: 使用字体将文本转换为图像的工具 +- `Styles Loader`: 加载 csv 文件并从行中填充下拉列表(类似于 A111) + +- `Smart Step`: 一个非常基本的节点,用于获取在 KSampler 高级中使用的步骤百分比 +- `Qr Code`: 基本的 QR Code 生成器 +- `Save Tensors`: 调试节点,将来可能会被删除 +- `Int to Number`: 用于 WASSuite 数字节点的补充 +- `Smart Step`: 使用百分比来控制 `KAdvancedSampler` 的步骤(开始/停止) + +### 纹理 + +- `DeepBump`: 从单张图片生成法线图和高度图 + +## Comfy 资源 + +**指南**: +- [官方示例(英文)](https://comfyanonymous.github.io/ComfyUI_examples/) +- @BlenderNeko 的[ComfyUI 社区手册(英文)](https://blenderneko.github.io/ComfyUI-docs/) + +- @tjhayasaka 的[Tomoaki 个人 Wiki(日文)](https://comfyui.creamlab.net/guides/) + +**扩展和自定义节点**: +- @WASasquatch 的[Comfy 列表插件(英文)](https://github.com/WASasquatch/comfyui-plugins) + +- [CivitAI 上的 ComfyUI 标签(英文)](https://civitai.com/tag/comfyui) \ No newline at end of file diff --git a/README-JP.md b/README-JP.md new file mode 100644 index 0000000..a48b5ef --- /dev/null +++ b/README-JP.md @@ -0,0 +1,101 @@ +## MTB Nodes + +Buy Me A Coffee + +MTB Nodesプロジェクトへようこそ!このコードベースは、自由に探索し、利用することができます。主な目的は、[MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs)の実装のための概念実証(POC)を構築することです。このプロジェクトの多くのノードは、既存のコミュニティの貢献や組み込みの機能に触発されています。 + +続行する前に、このプロジェクトで使用されている特定のライブラリに関連するライセンスに注意してください。たとえば、「deepbump」ライブラリは、[GPLv3](https://github.com/HugoTini/DeepBump/blob/master/LICENSE)の下でライセンスされています。 + +- [MTB Nodes](#mtb-nodes) +- [インストール](#インストール) +- [ノードリスト](#ノードリスト) + - [bbox](#bbox) + - [colors](#colors) + - [顔検出 / スワッピング](#顔検出--スワッピング) + - [画像補間(アニメーション)](#画像補間アニメーション) + - [画像操作](#画像操作) + - [潜在的なユーティリティ](#潜在的なユーティリティ) + - [その他のユーティリティ](#その他のユーティリティ) + - [テクスチャ](#テクスチャ) +- [Comfyリソース](#comfyリソース) + + +## インストール + +- [INSTALL-JP.md](./INSTALL-JP.md)に移動しました。 + + +## ノードリスト + +### bbox +- `Bounding Box`: BBoxコンストラクタ(カスタムタイプ) +- `BBox From Mask`: マスクからバウンディングボックスを抽出 +- `Crop`: BBoxから画像を切り抜く +- `Uncrop`: BBoxから画像を元に戻す + +### colors +- `Colored Image`: 指定されたサイズの一定の色の画像 +- `RGB to HSV`: - +- `HSV to RGB`: - +- `Color Correct`: 基本的なカラーコレクションツール + + +### 顔検出 / スワッピング +- `Face Swap`: deepinsight/insightfaceモデルを使用した顔の入れ替え(このノードは初期バージョンでは「Roop」と呼ばれていましたが、同じ機能を提供します。Roopは単にこれらのモデルを使用するアプリです) + > **注意** + > 顔のインデックスを使用して置き換える顔を選択できます。以下を参照してください: + +- `Load Face Swap Model`: 顔の交換のためのinsightfaceモデルを読み込む +- `Restore Face`: [GFPGan](https://github.com/TencentARC/GFPGAN)を使用して顔を復元し、`Face Swap`と組み合わせて使用すると非常に効果的であり、`bg_upscaler`のComfyネイティブアップスケーラーもサポートしています。 + +### 画像補間(アニメーション) +- `Load Film Model`: [FILM](https://github.com/google-research/frame-interpolation)モデルを読み込む +- `Film Interpolation`: [FILM](https://github.com/google-research/frame-interpolation)を使用して入力フレームを処理する + +- `Export to Prores (experimental)`: 入力フレームをProRes 4444 movファイルにエクスポートします。これは現在は単純なものですが、`Film Interpolation`と組み合わせて使用するためのffmpegのstdinを使用して生のNumPy配列を送信するもので、拡張することもできます。 + +### 画像操作 +- `Blur`: ガウスフィルタを使用して画像をぼかす +- `Deglaze Image`: [FN16](https://github.com/Fannovel16/FN16-ComfyUI-nodes/blob/main/DeglazeImage.py)から取得 +- `Denoise`: 入力画像のノイズを除去する +- `Image Compare`: 2つの画像を比較し、差分画像を返す +- `Image Premultiply`: 画像をマスクで乗算 +- `Image Remove Background Rembg`: [RemBG](https://github.com/danielgatis/rembg)を使用した背景除去 + +- `Image Resize Factor`: [WAS Node Suite](https://github.com/WASasquatch/was-node-suite-comfyui)から抽出され、いくつかの編集(特に複数の画像のサポート)と機能の削減が行われました。 +- `Mask To Image`: マスク(アルファ)をカラーと背景を持つRGBイメージに変換します。 +- `Save Image Grid`: 入力バッチのすべての画像を画像グリッドとして保存します。 + +### 潜在的なユーティリティ +- `Latent Lerp`: 2つの潜在的なベクトルの間の線形補間(ブレンド) + +### その他のユーティリティ +- `Concat Images`: 2つの画像ストリームを取り、他のComfyパイプラインでサポートされている画像のバッチとしてマージします。 +- `Image Resize Factor`: **非推奨**。組み込みの画像リサイズ機能を発見したため、削除される予定です。 +- `Text To Image`: フォントを使用してテキストを画像に変換するためのユーティリティ +- `Styles Loader`: csvファイルをロードし、行からドロップダウンを作成します(A111のようなもの) + +- `Smart Step`: KSamplerの高度な使用に使用するステップパーセントを取得する非常に基本的なノード +- `Qr Code`: 基本的なQRコード生成器 +- `Save Tensors`: 将来的に削除される可能性のあるデバッグノード +- `Int to Number`: WASSuiteの数値ノードの補完 +- `Smart Step`: `KAdvancedSampler`のステップ(開始/停止)を制御するための非常に基本的なツールで、パーセンテージを使用します。 + +### テクスチャ + +- `DeepBump`: 1枚の画像から法線マップと高さマップを生成します。 + +## Comfyリソース + +**ガイド**: +- [公式の例(英語)](https://comfyanonymous.github.io/ComfyUI_examples/) +- @BlenderNekoによる[ComfyUIコミュニティマニュアル(英語)](https://blenderneko.github.io/ComfyUI-docs/) + +- @tjhayasakaによる[Tomoakiの個人Wiki(日本語)](https://comfyui.creamlab.net/guides/) + +**拡張機能とカスタムノード**: +- @WASasquatchによる[Comfyリスト用のプラグイン(英語)](https://github.com/WASasquatch/comfyui-plugins) + +- [CivitAIのComfyUIタグ(英語)](https://civitai.com/tag/comfyui) \ No newline at end of file From f927bc7c9a82951e6df4763433732f20ea87e9cb Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Sun, 16 Jul 2023 23:30:30 +0200 Subject: [PATCH 07/70] =?UTF-8?q?chore:=20=E2=9C=A8=20add=20bug=20report?= =?UTF-8?q?=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.yml | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..fa10fcb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,67 @@ +name: 🐞 Bug Report +title: '[bug] ' +description: Report a bug +labels: ['type: 🐛 bug', 'status: 🧹 needs triage'] + +body: + - type: markdown + attributes: + value: | + ## Before submiting an issue + - Make sure to read the README & INSTALL instructions. + - Please search for [existing issues](https://github.com/melMass/comfy_mtb/issues?q=is%3Aissue) around your problem before filing a report. + + ### Try using the debug mode to get more info + + If you use the env variable `MTB_DEBUG=true`, debug message from the extension will appear in the terminal. + + - type: textarea + id: description + attributes: + label: Describe the bug + description: A clear description of what the bug is. Include screenshots if applicable. + placeholder: Bug description + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Steps to reproduce the behavior. + placeholder: | + 1. Add node xxx ... + 2. Connect to xxx ... + 3. See error + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear description of what you expected to happen. + + - type: textarea + id: info + attributes: + label: Platform and versions + description: 'informations about the environment you run Comfy in' + render: sh + placeholder: | + - OS: [e.g. Linux] + - Comfy Mode [e.g. custom env, standalone, google colab] + + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Console output + description: Paste the console output without backticks + render: sh + + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem here. From 710a638a8187ef08254478f684307dccdebcded2 Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Sun, 16 Jul 2023 23:36:42 +0200 Subject: [PATCH 08/70] =?UTF-8?q?chore:=20=E2=9C=A8=20add=20more=20issue?= =?UTF-8?q?=20templates=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.yml | 6 ++-- .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.yml | 35 ++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index fa10fcb..9288003 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,7 @@ name: 🐞 Bug Report -title: '[bug] ' +title: "[bug] " description: Report a bug -labels: ['type: 🐛 bug', 'status: 🧹 needs triage'] +labels: ["type: 🐛 bug", "status: 🧹 needs triage"] body: - type: markdown @@ -44,7 +44,7 @@ body: id: info attributes: label: Platform and versions - description: 'informations about the environment you run Comfy in' + description: "informations about the environment you run Comfy in" render: sh placeholder: | - OS: [e.g. Linux] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..0080859 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,35 @@ +name: 💡 Feature Request +title: "[feat] " +description: Suggest an idea +labels: ["type: 🤚 feature request"] + +body: + - type: textarea + id: problem + attributes: + label: Describe the problem + description: A clear description of the problem this feature would solve + placeholder: "I'm always frustrated when..." + validations: + required: true + + - type: textarea + id: solution + attributes: + label: "Describe the solution you'd like" + description: A clear description of what change you would like + placeholder: "I would like to..." + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: "Any alternative solutions you've considered" + + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem here. From 38f61473bc23b4c5d4efc5048d54c059565a6fa0 Mon Sep 17 00:00:00 2001 From: melMass Date: Sun, 16 Jul 2023 23:41:36 +0200 Subject: [PATCH 09/70] =?UTF-8?q?feat:=20=E2=9C=A8=20extract=20node=20name?= =?UTF-8?q?s=20using=20ast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In case a file can't be loaded this still allow to get the expected nodes that would be loaded --- __init__.py | 42 ++++++++++++++++++++++++++++++++++++++---- utils.py | 1 - 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index 6f28319..ec213b3 100644 --- a/__init__.py +++ b/__init__.py @@ -7,15 +7,43 @@ from .log import log, blue_text, cyan_text, get_summary, get_label from .utils import here import importlib import os +import ast NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} NODE_CLASS_MAPPINGS_DEBUG = {} +def extract_nodes_from_source(source_code): + nodes = [] + try: + parsed = ast.parse(source_code) + for node in ast.walk(parsed): + if isinstance(node, ast.Assign) and len(node.targets) == 1: + target = node.targets[0] + if isinstance(target, ast.Name) and target.id == "__nodes__": + value = ast.get_source_segment(source_code, node.value) + node_value = ast.parse(value).body[0].value + if isinstance(node_value, ast.List) or isinstance( + node_value, ast.Tuple + ): + for element in node_value.elts: + if isinstance(element, ast.Name): + print(element.id) + nodes.append(element.id) + + break + except SyntaxError: + log.error("Failed to parse") + pass # File couldn't be parsed + return nodes + + def load_nodes(): errors = [] nodes = [] + nodes_failed = [] + for filename in (here / "nodes").iterdir(): if filename.suffix == ".py": module_name = filename.stem @@ -26,14 +54,20 @@ def load_nodes(): ) _nodes = getattr(module, "__nodes__") nodes.extend(_nodes) - log.debug(f"Imported {module_name} nodes") except AttributeError: pass # wip nodes except Exception: error_message = traceback.format_exc().splitlines()[-1] - errors.append(f"Failed to import {module_name} because {error_message}") + errors.append( + f"Failed to import module {module_name} because {error_message}" + ) + # Read __nodes__ variable from the source file + with open(filename, "r") as file: + source_code = file.read() + extracted_nodes = extract_nodes_from_source(source_code) + nodes_failed.extend(extracted_nodes) if errors: log.error( @@ -44,7 +78,7 @@ def load_nodes(): + "If you think this is a bug, please report it on the github page (https://github.com/melMass/comfy_mtb/issues)" ) - return nodes + return (nodes, nodes_failed) # - REGISTER WEB EXTENSIONS @@ -66,7 +100,7 @@ else: ) # - REGISTER NODES -nodes = load_nodes() +nodes, failed = load_nodes() for node_class in nodes: class_name = node_class.__name__ node_label = f"{get_label(class_name)} (mtb)" diff --git a/utils.py b/utils.py index f90be5b..0a93e7f 100644 --- a/utils.py +++ b/utils.py @@ -88,4 +88,3 @@ def tensor2np(tensor: torch.Tensor) -> Union[np.ndarray, List[np.ndarray]]: return out return [np.clip(255.0 * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)] - From cde72938d5ffd09179f5974676e12d4599a8d6ff Mon Sep 17 00:00:00 2001 From: melMass Date: Sun, 16 Jul 2023 23:42:45 +0200 Subject: [PATCH 10/70] =?UTF-8?q?feat:=20=E2=9C=A8=20WIP=20batch=20from=20?= =?UTF-8?q?history?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Barely tested, and not much safe guards (I need to analyze more history sessions first, but the basic idea is there) --- nodes/image_interpolation.py | 99 ++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 4 deletions(-) diff --git a/nodes/image_interpolation.py b/nodes/image_interpolation.py index cfaf337..b5b5c09 100644 --- a/nodes/image_interpolation.py +++ b/nodes/image_interpolation.py @@ -11,9 +11,94 @@ import uuid import numpy as np import subprocess import comfy - +from PIL import Image +import urllib.request +import urllib.parse +import json import tensorflow as tf import comfy.model_management as model_management +import io + +from comfy.cli_args import args +from ..utils import pil2tensor + + +def get_image(filename, subfolder, folder_type): + data = {"filename": filename, "subfolder": subfolder, "type": folder_type} + url_values = urllib.parse.urlencode(data) + with urllib.request.urlopen( + "http://{}:{}/view?{}".format(args.listen, args.port, url_values) + ) as response: + return io.BytesIO(response.read()) + + +class GetBatchFromHistory: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "enable": ("BOOL", {"default": True}), + "count": ("INT", {"default": 1, "min": 0}), + "offset": ("INT", {"default": 0, "min": -1e9, "max": 1e9}), + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = "images" + CATEGORY = "animation" + FUNCTION = "load_from_history" + + def load_from_history( + self, + enable=True, + count=0, + offset=0, + ): + if not enable or count == 0: + return (torch.zeros(0),) + frames = [] + server_address = "localhost:3000" + + with urllib.request.urlopen( + "http://{}/history".format(server_address) + ) as response: + history = json.loads(response.read()) + + output_images = [] + for k, run in history.items(): + for o in run["outputs"]: + for node_id in run["outputs"]: + node_output = run["outputs"][node_id] + if "images" in node_output: + images_output = [] + for image in node_output["images"]: + image_data = get_image( + image["filename"], image["subfolder"], image["type"] + ) + images_output.append(image_data) + output_images.extend(images_output) + if len(output_images) == 0: + return (torch.zeros(0),) + for i, image in enumerate(list(reversed(output_images))): + if i < offset: + continue + if i >= offset + count: + break + # Decode image as tensor + img = Image.open(image) + log.debug(f"Image from history {i} of shape {img.size}") + frames.append(img) + + # Display the shape of the tensor + # print("Tensor shape:", image_tensor.shape) + + # return (output_images,) + + output = pil2tensor( + list(reversed(frames)), + ) + + return (output,) class LoadFilmModel: @@ -160,7 +245,7 @@ class ConcatImages: return (self.concatenate_tensors(imageA, imageB),) -class ExportToProRes: +class ExportToProres: """Export to ProRes 4444 (Experimental)""" def __init__(self): @@ -196,7 +281,7 @@ class ExportToProRes: log.debug(f"Exporting to {output_dir / id}") frames = tensor2np(images) - log.debug(f"Frames type {type(frames)}") + log.debug(f"Frames type {type(frames[0])}") log.debug(f"Exporting {len(frames)} frames") frames = [frame.astype(np.uint16) * 257 for frame in frames] @@ -245,4 +330,10 @@ class ExportToProRes: return (out_path,) -__nodes__ = [LoadFilmModel, FilmInterpolation, ExportToProRes, ConcatImages] +__nodes__ = [ + LoadFilmModel, + FilmInterpolation, + ExportToProres, + ConcatImages, + GetBatchFromHistory, +] From 6168b3a2ac38b5eebed3daf9e52df5742abf6813 Mon Sep 17 00:00:00 2001 From: melMass Date: Mon, 17 Jul 2023 22:52:25 +0200 Subject: [PATCH 11/70] =?UTF-8?q?fix:=20=E2=9A=A1=EF=B8=8F=20from=20tensor?= =?UTF-8?q?2np=20always=20returning=20a=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 21 ++++++++++++--------- nodes/faceenhance.py | 7 +++---- nodes/faceswap.py | 4 ++-- utils.py | 2 +- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/__init__.py b/__init__.py index ec213b3..03c9f48 100644 --- a/__init__.py +++ b/__init__.py @@ -14,8 +14,14 @@ NODE_DISPLAY_NAME_MAPPINGS = {} NODE_CLASS_MAPPINGS_DEBUG = {} -def extract_nodes_from_source(source_code): +def extract_nodes_from_source(filename): + source_code = "" + + with open(filename, "r") as file: + source_code = file.read() + nodes = [] + try: parsed = ast.parse(source_code) for node in ast.walk(parsed): @@ -36,6 +42,7 @@ def extract_nodes_from_source(source_code): except SyntaxError: log.error("Failed to parse") pass # File couldn't be parsed + return nodes @@ -64,13 +71,10 @@ def load_nodes(): f"Failed to import module {module_name} because {error_message}" ) # Read __nodes__ variable from the source file - with open(filename, "r") as file: - source_code = file.read() - extracted_nodes = extract_nodes_from_source(source_code) - nodes_failed.extend(extracted_nodes) + nodes_failed.extend(extract_nodes_from_source(filename)) if errors: - log.error( + log.info( f"Some nodes failed to load:\n\t" + "\n\t".join(errors) + "\n\n" @@ -91,11 +95,11 @@ elif web_extensions_root.exists(): try: os.symlink((here / "web"), web_mtb.as_posix()) except Exception: # OSError - log.error( + log.warn( f"Failed to create symlink to {web_mtb}. Please copy the folder manually." ) else: - log.error( + log.warn( f"Comfy root probably not found automatically, please copy the folder {web_mtb} manually in the web/extensions folder of ComfyUI" ) @@ -126,4 +130,3 @@ MANIFEST = { "project": "https://github.com/melMass/comfy_mtb", # The address that the `name` value will link to on Node Class Views "description": "Set of nodes that enhance your animation workflow and provide a range of useful tools including features such as manipulating bounding boxes, perform color corrections, swap faces in images, interpolate frames for smooth animation, export to ProRes format, apply various image operations, work with latent spaces, generate QR codes, and create normal and height maps for textures.", } - diff --git a/nodes/faceenhance.py b/nodes/faceenhance.py index 33296eb..95a55f9 100644 --- a/nodes/faceenhance.py +++ b/nodes/faceenhance.py @@ -111,7 +111,7 @@ class BGUpscaleWrapper: self.upscale_model.cpu() s = torch.clamp(s.movedim(-3, -1), min=0, max=1.0) - return (tensor2np(s),) + return (tensor2np(s)[0],) import sys @@ -150,9 +150,8 @@ class RestoreFace: weight, save_tmp_steps, ) -> torch.Tensor: - pimage = tensor2pil(image) - width, height = pimage.size - + pimage = tensor2np(image)[0] + width, height = pimage.shape[1], pimage.shape[0] source_img = cv2.cvtColor(np.array(pimage), cv2.COLOR_RGB2BGR) sys.stdout = NullWriter() diff --git a/nodes/faceswap.py b/nodes/faceswap.py index 8dcf7ac..0a154ba 100644 --- a/nodes/faceswap.py +++ b/nodes/faceswap.py @@ -100,8 +100,8 @@ class FaceSwap: ): def do_swap(img): model_management.throw_exception_if_processing_interrupted() - img = tensor2pil(img) - ref = tensor2pil(reference) + img = tensor2pil(img)[0] + ref = tensor2pil(reference)[0] face_ids = { int(x) for x in faces_index.strip(",").split(",") if x.isnumeric() } diff --git a/utils.py b/utils.py index 0a93e7f..cafe31d 100644 --- a/utils.py +++ b/utils.py @@ -77,7 +77,7 @@ def np2tensor(img_np: np.ndarray | List[np.ndarray]) -> torch.Tensor: return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0) -def tensor2np(tensor: torch.Tensor) -> Union[np.ndarray, List[np.ndarray]]: +def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]: batch_count = 1 if len(tensor.shape) > 3: batch_count = tensor.size(0) From 5f5297f80debc77f3fda2f0d37b3acff8419140d Mon Sep 17 00:00:00 2001 From: melMass Date: Tue, 18 Jul 2023 03:08:52 +0200 Subject: [PATCH 12/70] =?UTF-8?q?feat:=20=E2=9C=A8=20added=20a=20way=20to?= =?UTF-8?q?=20export=20the=20node=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is still wip to see what could fit ComfyUI-manager --- __init__.py | 13 +++++++++++++ node_list.json | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 node_list.json diff --git a/__init__.py b/__init__.py index 03c9f48..b780a99 100644 --- a/__init__.py +++ b/__init__.py @@ -8,6 +8,7 @@ from .utils import here import importlib import os import ast +import json NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} @@ -114,6 +115,18 @@ for node_class in nodes: # TODO: I removed this, I find it more convenient to write without spaces, but it breaks every of my workflows # TODO (cont): and until I find a way to automate the conversion, I'll leave it like this + if os.environ.get("MTB_EXPORT"): + with open(here / "node_list.json", "w") as f: + f.write( + json.dumps( + { + k: NODE_CLASS_MAPPINGS_DEBUG[k] + for k in sorted(NODE_CLASS_MAPPINGS_DEBUG.keys()) + }, + indent=4, + ) + ) + log.info( f"Loaded the following nodes:\n\t" + "\n\t".join( diff --git a/node_list.json b/node_list.json new file mode 100644 index 0000000..422826f --- /dev/null +++ b/node_list.json @@ -0,0 +1,45 @@ +{ + "B Box From Mask (mtb)": "From a mask extract the bounding box", + "Blur (mtb)": "Blur an image using a Gaussian filter.", + "Bounding Box (mtb)": "The bounding box (BBOX) custom type used by other nodes", + "Color Correct (mtb)": "Various color correction methods", + "Colored Image (mtb)": "Constant color image of given size", + "Concat Images (mtb)": "Add images to batch", + "Crop (mtb)": "Crops an image and an optional mask to a given bounding box\n\n The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type\n The BBOX input takes precedence over the tuple input\n ", + "Deep Bump (mtb)": "Normal & height maps generation from single pictures", + "Deglaze Image (mtb)": "Remove adversarial noise from images", + "Denoise (mtb)": "Denoise an image using total variation minimization.", + "Export To Prores (mtb)": "Export to ProRes 4444 (Experimental)", + "Face Swap (mtb)": "Face swap using deepinsight/insightface models", + "Film Interpolation (mtb)": "Google Research FILM frame interpolation for large motion", + "Float To Number (mtb)": "Node addon for the WAS Suite. Converts a \"comfy\" FLOAT to a NUMBER.", + "Get Batch From History (mtb)": "Experimental node to load images from the history of the server.\n\n Queue item without output are ignore in the count.", + "Hsv To Rgb (mtb)": "Convert HSV image to RGB", + "Image Compare (mtb)": "Compare two images and return a difference image", + "Image Premultiply (mtb)": "Premultiply image with mask", + "Image Remove Background Rembg (mtb)": "Removes the background from the input using Rembg.", + "Image Resize Factor (mtb)": "Extracted mostly from WAS Node Suite, with a few edits (most notably multiple image support) and less features.", + "Int To Bool (mtb)": "Basic int to bool conversion", + "Int To Number (mtb)": "Node addon for the WAS Suite. Converts a \"comfy\" INT to a NUMBER.", + "Latent Lerp (mtb)": "Linear interpolation (blend) between two latent vectors", + "Latent Noise (mtb)": "Inject noise into latent space", + "Latent Transform (mtb)": "Dumb attempt at reproducing some deforum like motion", + "Load Face Enhance Model (mtb)": "Loads a GFPGan or RestoreFormer model for face enhancement.", + "Load Face Swap Model (mtb)": "Loads a faceswap model", + "Load Film Model (mtb)": "Loads a FILM model", + "Load Image From Url (mtb)": "Load an image from the given URL", + "Load Image Sequence (mtb)": "Load an image sequence from a folder. The current frame is used to determine which image to load.\n\n Usually used in conjunction with the `Primitive` node set to increment to load a sequence of images from a folder.\n Use -1 to load all matching frames as a batch.\n ", + "Mask To Image (mtb)": "Converts a mask (alpha) to an RGB image with a color and background", + "Mtb Examples (mtb)": "MTB Example Images", + "Qr Code (mtb)": "Basic QR Code generator", + "Restore Face (mtb)": "Uses GFPGan to restore faces", + "Rgb To Hsv (mtb)": "Convert RGB image to HSV", + "Save Image Grid (mtb)": "Save all the images in the input batch as a grid of images.", + "Save Image Sequence (mtb)": "Save an image sequence to a folder. The current frame is used to determine which image to save.\n\n This is merely a wrapper around the `save_images` function with formatting for the output folder and filename.\n ", + "Save Tensors (mtb)": "Debug node that will probably be removed in the future", + "Smart Step (mtb)": "Utils to control the steps start/stop of the KAdvancedSampler in percentage", + "String Replace (mtb)": "Basic string replacement", + "Styles Loader (mtb)": "Load csv files and populate a dropdown from the rows (\u00e0 la A111)", + "Text To Image (mtb)": "Utils to convert text to image using a font\n\n\n The tool looks for any .ttf file in the Comfy folder hierarchy.\n ", + "Uncrop (mtb)": "Uncrops an image to a given bounding box\n\n The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type\n The BBOX input takes precedence over the tuple input" +} \ No newline at end of file From ab09ccadd905bebbf1b7b2d992e96e36fc60d68a Mon Sep 17 00:00:00 2001 From: melMass Date: Tue, 18 Jul 2023 03:13:32 +0200 Subject: [PATCH 13/70] =?UTF-8?q?fix:=20=E2=9A=A1=EF=B8=8F=20a=20few=20mis?= =?UTF-8?q?sing=20=5F=5Fdoc=5F=5F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/faceenhance.py | 4 ++ nodes/image_interpolation.py | 4 ++ nodes/image_processing.py | 34 ++++++++++++-- nodes/mask.py | 90 +++++++++++++++++++++++++----------- nodes/number.py | 71 +++++++++++++++++++++++++++- 5 files changed, 172 insertions(+), 31 deletions(-) diff --git a/nodes/faceenhance.py b/nodes/faceenhance.py index 95a55f9..00c33c0 100644 --- a/nodes/faceenhance.py +++ b/nodes/faceenhance.py @@ -16,6 +16,8 @@ from typing import Tuple class LoadFaceEnhanceModel: + """Loads a GFPGan or RestoreFormer model for face enhancement.""" + def __init__(self) -> None: pass @@ -118,6 +120,8 @@ import sys class RestoreFace: + """Uses GFPGan to restore faces""" + def __init__(self) -> None: pass diff --git a/nodes/image_interpolation.py b/nodes/image_interpolation.py index b5b5c09..d74cf63 100644 --- a/nodes/image_interpolation.py +++ b/nodes/image_interpolation.py @@ -33,6 +33,10 @@ def get_image(filename, subfolder, folder_type): class GetBatchFromHistory: + """Experimental node to load images from the history of the server. + + Queue item without output are ignore in the count.""" + @classmethod def INPUT_TYPES(cls): return { diff --git a/nodes/image_processing.py b/nodes/image_processing.py index e277dd1..394523a 100644 --- a/nodes/image_processing.py +++ b/nodes/image_processing.py @@ -276,6 +276,35 @@ class ImageCompare: return (torch.from_numpy(image),) +import requests + + +class LoadImageFromUrl: + """Load an image from the given URL""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "url": ( + "STRING", + { + "default": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Example.jpg/800px-Example.jpg" + }, + ), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "load" + CATEGORY = "image" + + def load(self, url): + # get the image from the url + image = Image.open(requests.get(url, stream=True).raw) + return (pil2tensor(image),) + + class Denoise: """Denoise an image using total variation minimization.""" @@ -477,9 +506,7 @@ class ImagePremultiply: class ImageResizeFactor: - """ - Extracted mostly from WAS Node Suite, with a few edits (most notably multiple image support) and less features. - """ + """Extracted mostly from WAS Node Suite, with a few edits (most notably multiple image support) and less features.""" def __init__(self): pass @@ -717,4 +744,5 @@ __nodes__ = [ ImagePremultiply, ImageResizeFactor, SaveImageGrid, + LoadImageFromUrl, ] diff --git a/nodes/mask.py b/nodes/mask.py index 828478d..fac0ac0 100644 --- a/nodes/mask.py +++ b/nodes/mask.py @@ -2,7 +2,10 @@ from rembg import remove from ..utils import pil2tensor, tensor2pil from PIL import Image + class ImageRemoveBackgroundRembg: + """Removes the background from the input using Rembg.""" + def __init__(self): pass @@ -11,47 +14,80 @@ class ImageRemoveBackgroundRembg: return { "required": { "image": ("IMAGE",), - "alpha_matting": (["True","False"], {"default":"False"},), - "alpha_matting_foreground_threshold": ("INT", {"default":240, "min": 0, "max": 255},), - "alpha_matting_background_threshold": ("INT", {"default":10, "min": 0, "max": 255},), - "alpha_matting_erode_size": ("INT", {"default":10, "min": 0, "max": 255},), - "post_process_mask": (["True","False"], {"default":"False"},), - "bgcolor": ("COLOR", {"default":"black"},), - + "alpha_matting": ( + ["True", "False"], + {"default": "False"}, + ), + "alpha_matting_foreground_threshold": ( + "INT", + {"default": 240, "min": 0, "max": 255}, + ), + "alpha_matting_background_threshold": ( + "INT", + {"default": 10, "min": 0, "max": 255}, + ), + "alpha_matting_erode_size": ( + "INT", + {"default": 10, "min": 0, "max": 255}, + ), + "post_process_mask": ( + ["True", "False"], + {"default": "False"}, + ), + "bgcolor": ( + "COLOR", + {"default": "black"}, + ), }, } - RETURN_TYPES = ("IMAGE","MASK","IMAGE",) - RETURN_NAMES = ("Image (rgba)","Mask","Image",) + RETURN_TYPES = ( + "IMAGE", + "MASK", + "IMAGE", + ) + RETURN_NAMES = ( + "Image (rgba)", + "Mask", + "Image", + ) FUNCTION = "remove_background" CATEGORY = "image" + # bgcolor: Optional[Tuple[int, int, int, int]] - def remove_background(self, image, alpha_matting, alpha_matting_foreground_threshold, alpha_matting_background_threshold, alpha_matting_erode_size, post_process_mask, bgcolor): + def remove_background( + self, + image, + alpha_matting, + alpha_matting_foreground_threshold, + alpha_matting_background_threshold, + alpha_matting_erode_size, + post_process_mask, + bgcolor, + ): image = remove( - data=tensor2pil(image), - alpha_matting=alpha_matting == "True", - alpha_matting_foreground_threshold=alpha_matting_foreground_threshold, - alpha_matting_background_threshold=alpha_matting_background_threshold, - alpha_matting_erode_size=alpha_matting_erode_size, - session=None, - only_mask=False, - post_process_mask=post_process_mask == "True", - bgcolor=None - ) - - + data=tensor2pil(image), + alpha_matting=alpha_matting == "True", + alpha_matting_foreground_threshold=alpha_matting_foreground_threshold, + alpha_matting_background_threshold=alpha_matting_background_threshold, + alpha_matting_erode_size=alpha_matting_erode_size, + session=None, + only_mask=False, + post_process_mask=post_process_mask == "True", + bgcolor=None, + ) + # extract the alpha to a new image mask = image.getchannel(3) - + # add our bgcolor behind the image image_on_bg = Image.new("RGBA", image.size, bgcolor) - + image_on_bg.paste(image, mask=mask) - - + return (pil2tensor(image), pil2tensor(mask), pil2tensor(image_on_bg)) __nodes__ = [ ImageRemoveBackgroundRembg, -] \ No newline at end of file +] diff --git a/nodes/number.py b/nodes/number.py index cf6c6a1..5113a5a 100644 --- a/nodes/number.py +++ b/nodes/number.py @@ -1,3 +1,27 @@ +class IntToBool: + """Basic int to bool conversion""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "int": ( + "INT", + { + "default": 0, + }, + ), + } + } + + RETURN_TYPES = ("BOOL",) + FUNCTION = "int_to_bool" + CATEGORY = "number" + + def int_to_bool(self, int): + return (bool(int),) + + class IntToNumber: """Node addon for the WAS Suite. Converts a "comfy" INT to a NUMBER.""" @@ -8,7 +32,16 @@ class IntToNumber: def INPUT_TYPES(cls): return { "required": { - "int": ("INT", {"default": 0, "min": 0, "max": 1e9, "step": 1}), + "int": ( + "INT", + { + "default": 0, + "min": -1e9, + "max": 1e9, + "step": 1, + "forceInput": True, + }, + ), } } @@ -17,10 +50,46 @@ class IntToNumber: CATEGORY = "number" def int_to_number(self, int): + return (int,) + + +class FloatToNumber: + """Node addon for the WAS Suite. Converts a "comfy" FLOAT to a NUMBER.""" + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "float": ( + "FLOAT", + { + "default": 0, + "min": -1e9, + "max": 1e9, + "step": 1, + "forceInput": True, + }, + ), + } + } + + RETURN_TYPES = ("NUMBER",) + FUNCTION = "float_to_number" + CATEGORY = "number" + + def float_to_number(self, float): + return (float,) + + return (int,) __nodes__ = [ + FloatToNumber, + IntToBool, IntToNumber, ] \ No newline at end of file From b9c1d3df7a1460fe9ffa84f6f9ea0cfb5409de1a Mon Sep 17 00:00:00 2001 From: melMass Date: Tue, 18 Jul 2023 03:37:30 +0200 Subject: [PATCH 14/70] =?UTF-8?q?feat:=20=F0=9F=9A=80=20add=20a=20few=20ex?= =?UTF-8?q?amples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/01-faceswap.json | 1012 ++++++++++++++++++++ examples/02-film_interpolation.json | 1349 +++++++++++++++++++++++++++ 2 files changed, 2361 insertions(+) create mode 100644 examples/01-faceswap.json create mode 100644 examples/02-film_interpolation.json diff --git a/examples/01-faceswap.json b/examples/01-faceswap.json new file mode 100644 index 0000000..605697a --- /dev/null +++ b/examples/01-faceswap.json @@ -0,0 +1,1012 @@ +{ + "last_node_id": 76, + "last_link_id": 161, + "nodes": [ + { + "id": 59, + "type": "Reroute", + "pos": [ + -150.35178124999982, + 644.4360633544919 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 124 + } + ], + "outputs": [ + { + "name": "", + "type": "VAE", + "links": [ + 139 + ], + "slot_index": 0 + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 56, + "type": "Reroute", + "pos": [ + -1580.8297949218763, + 644.7740239257807 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 117 + } + ], + "outputs": [ + { + "name": "", + "type": "VAE", + "links": [ + 124 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 57, + "type": "Reroute", + "pos": [ + -673.8297949218747, + -185.22597607421872 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 135 + } + ], + "outputs": [ + { + "name": "", + "type": "MODEL", + "links": [ + 120 + ], + "slot_index": 0 + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 65, + "type": "Reroute", + "pos": [ + -1512.8297949218763, + -181.22597607421872 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 134 + } + ], + "outputs": [ + { + "name": "", + "type": "MODEL", + "links": [ + 135 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + -834.8297949218747, + 206.77402392578134 + ], + "size": [ + 210, + 54 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 142 + }, + { + "name": "text", + "type": "STRING", + "link": 87, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 6 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "worst quality, hands, embedding:EasyNegative," + ] + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": [ + -913.8297949218747, + 326.77402392578125 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 2 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 768, + 512, + 1 + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + -1040, + -2 + ], + "size": [ + 422.84503173828125, + 164.31304931640625 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 141 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 4 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "Closeup portrait of an old bearded Caucasian man smiling, (NYC 1995), trench coat, golden ring, brown eyes" + ] + }, + { + "id": 16, + "type": "CheckpointLoaderSimple", + "pos": [ + -2001, + 193 + ], + "size": [ + 315, + 98 + ], + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 134 + ], + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 141, + 142 + ], + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 117 + ], + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "revAnimated_v122.safetensors" + ] + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + -483, + -21 + ], + "size": [ + 315, + 474 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 120 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 4 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 6 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 2 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 138 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 542821171532982, + "fixed", + 45, + 8, + "dpmpp_sde", + "simple", + 1 + ] + }, + { + "id": 66, + "type": "VAEDecodeTiled", + "pos": [ + 205, + -28 + ], + "size": [ + 210, + 46 + ], + "flags": { + "collapsed": false + }, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 138 + }, + { + "name": "vae", + "type": "VAE", + "link": 139, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 153, + 161 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecodeTiled" + } + }, + { + "id": 25, + "type": "PreviewImage", + "pos": [ + 2338.9229387812507, + -632.8275743593749 + ], + "size": [ + 726.28564453125, + 475.3432312011719 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 150 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 75, + "type": "PreviewImage", + "pos": [ + 1573.5946044921875, + -629.80322265625 + ], + "size": [ + 691.7459716796875, + 479.6098327636719 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 156 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 70, + "type": "Restore Face (mtb)", + "pos": [ + 1899, + -51 + ], + "size": [ + 315, + 150 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 149 + }, + { + "name": "model", + "type": "FACEENHANCE_MODEL", + "link": 151, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 150 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Restore Face (mtb)" + }, + "widgets_values": [ + "false", + "false", + 0.5, + "true" + ] + }, + { + "id": 67, + "type": "Face Swap (mtb)", + "pos": [ + 1127, + -26 + ], + "size": [ + 315, + 122 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 161 + }, + { + "name": "reference", + "type": "IMAGE", + "link": 160, + "slot_index": 1 + }, + { + "name": "faceswap_model", + "type": "FACESWAP_MODEL", + "link": 147, + "slot_index": 2 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 149, + 156 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Face Swap (mtb)" + }, + "widgets_values": [ + "0", + "false" + ] + }, + { + "id": 71, + "type": "Load Face Enhance Model (mtb)", + "pos": [ + 1476, + 240 + ], + "size": [ + 315, + 82 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "bg_upsampler", + "type": "UPSCALE_MODEL", + "link": 152, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "model", + "type": "FACEENHANCE_MODEL", + "links": [ + 151 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "Load Face Enhance Model (mtb)" + }, + "widgets_values": [ + "GFPGANv1.4.pth", + 2 + ] + }, + { + "id": 72, + "type": "UpscaleModelLoader", + "pos": [ + 1110, + 241 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "UPSCALE_MODEL", + "type": "UPSCALE_MODEL", + "links": [ + 152 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "UpscaleModelLoader" + }, + "widgets_values": [ + "4x-UltraSharp.pth" + ] + }, + { + "id": 73, + "type": "PreviewImage", + "pos": [ + 760, + -630 + ], + "size": [ + 671.1859741210938, + 483.1548156738281 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 153 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 76, + "type": "Load Image From Url (mtb)", + "pos": [ + 161, + 231 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 3, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 160 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Load Image From Url (mtb)" + }, + "widgets_values": [ + "https://lucasmuseum.org/assets/general/Lucas_Headshot_Color_web.jpg" + ] + }, + { + "id": 69, + "type": "Load Face Swap Model (mtb)", + "pos": [ + 172, + 503 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 4, + "mode": 0, + "outputs": [ + { + "name": "FACESWAP_MODEL", + "type": "FACESWAP_MODEL", + "links": [ + 147 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "Load Face Swap Model (mtb)" + }, + "widgets_values": [ + "inswapper_128.onnx" + ] + }, + { + "id": 20, + "type": "Styles Loader (mtb)", + "pos": [ + -1341, + 276 + ], + "size": [ + 315, + 78 + ], + "flags": { + "collapsed": false + }, + "order": 5, + "mode": 0, + "outputs": [ + { + "name": "positive", + "type": "STRING", + "links": [], + "shape": 3 + }, + { + "name": "negative", + "type": "STRING", + "links": [ + 87 + ], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "Styles Loader (mtb)" + }, + "widgets_values": [ + "❌Low Token" + ] + } + ], + "links": [ + [ + 2, + 5, + 0, + 3, + 3, + "LATENT" + ], + [ + 4, + 6, + 0, + 3, + 1, + "CONDITIONING" + ], + [ + 6, + 7, + 0, + 3, + 2, + "CONDITIONING" + ], + [ + 87, + 20, + 1, + 7, + 1, + "STRING" + ], + [ + 117, + 16, + 2, + 56, + 0, + "*" + ], + [ + 120, + 57, + 0, + 3, + 0, + "MODEL" + ], + [ + 124, + 56, + 0, + 59, + 0, + "*" + ], + [ + 134, + 16, + 0, + 65, + 0, + "*" + ], + [ + 135, + 65, + 0, + 57, + 0, + "*" + ], + [ + 138, + 3, + 0, + 66, + 0, + "LATENT" + ], + [ + 139, + 59, + 0, + 66, + 1, + "VAE" + ], + [ + 141, + 16, + 1, + 6, + 0, + "CLIP" + ], + [ + 142, + 16, + 1, + 7, + 0, + "CLIP" + ], + [ + 147, + 69, + 0, + 67, + 2, + "FACESWAP_MODEL" + ], + [ + 149, + 67, + 0, + 70, + 0, + "IMAGE" + ], + [ + 150, + 70, + 0, + 25, + 0, + "IMAGE" + ], + [ + 151, + 71, + 0, + 70, + 1, + "FACEENHANCE_MODEL" + ], + [ + 152, + 72, + 0, + 71, + 0, + "UPSCALE_MODEL" + ], + [ + 153, + 66, + 0, + 73, + 0, + "IMAGE" + ], + [ + 156, + 67, + 0, + 75, + 0, + "IMAGE" + ], + [ + 160, + 76, + 0, + 67, + 1, + "IMAGE" + ], + [ + 161, + 66, + 0, + 67, + 0, + "IMAGE" + ] + ], + "groups": [ + { + "title": "Txt2Img", + "bounding": [ + -2061, + -234, + 1932, + 973 + ], + "color": "#a1309b", + "locked": false + }, + { + "title": "Save Intermediate Image", + "bounding": [ + 147, + -152, + 303, + 213 + ], + "color": "#3f789e", + "locked": false + }, + { + "title": "SWAP & RESTORED", + "bounding": [ + 2305, + -741, + 789, + 638 + ], + "color": "#3f789e", + "locked": false + }, + { + "title": "SWAP", + "bounding": [ + 1520, + -743, + 774, + 642 + ], + "color": "#3f789e", + "locked": false + }, + { + "title": "SD OUTPUT", + "bounding": [ + 655, + -745, + 854, + 648 + ], + "color": "#3f789e", + "locked": false + } + ], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file diff --git a/examples/02-film_interpolation.json b/examples/02-film_interpolation.json new file mode 100644 index 0000000..218de4f --- /dev/null +++ b/examples/02-film_interpolation.json @@ -0,0 +1,1349 @@ +{ + "last_node_id": 82, + "last_link_id": 165, + "nodes": [ + { + "id": 59, + "type": "Reroute", + "pos": [ + -670, + 980 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 124 + } + ], + "outputs": [ + { + "name": "", + "type": "VAE", + "links": [ + 139, + 154 + ], + "slot_index": 0 + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 56, + "type": "Reroute", + "pos": [ + -2050, + 980 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 117 + } + ], + "outputs": [ + { + "name": "", + "type": "VAE", + "links": [ + 124 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 65, + "type": "Reroute", + "pos": [ + -1990, + 160 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 134 + } + ], + "outputs": [ + { + "name": "", + "type": "MODEL", + "links": [ + 135 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 57, + "type": "Reroute", + "pos": [ + -1200, + 150 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 135 + } + ], + "outputs": [ + { + "name": "", + "type": "MODEL", + "links": [ + 120, + 143 + ], + "slot_index": 0 + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": [ + -1410, + 660 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 2, + 153 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 768, + 512, + 1 + ] + }, + { + "id": 67, + "type": "Text", + "pos": [ + -2280, + 250 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 142, + 164 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Text" + }, + "widgets_values": [ + "Photo of a Face of a man (looking down), rim lighting, tokyo 1987" + ] + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + -1010, + 320 + ], + "size": [ + 315, + 474 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 120 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 4 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 6 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 2 + }, + { + "name": "seed", + "type": "INT", + "link": 148, + "widget": { + "name": "seed", + "config": [ + "INT", + { + "default": 0, + "min": 0, + "max": 18446744073709552000 + } + ] + } + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 138 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 542821171532978, + "fixed", + 45, + 8, + "euler_ancestral", + "simple", + 1 + ] + }, + { + "id": 72, + "type": "PrimitiveNode", + "pos": [ + -1290, + 880 + ], + "size": [ + 210, + 82 + ], + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 148, + 150 + ], + "widget": { + "name": "seed", + "config": [ + "INT", + { + "default": 0, + "min": 0, + "max": 18446744073709552000 + } + ] + }, + "slot_index": 0 + } + ], + "title": "seed", + "properties": {}, + "widgets_values": [ + 542821171532978, + "fixed" + ] + }, + { + "id": 79, + "type": "Load Film Model (mtb)", + "pos": [ + 720, + 590 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 3, + "mode": 0, + "outputs": [ + { + "name": "FILM_MODEL", + "type": "FILM_MODEL", + "links": [ + 161 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "Load Film Model (mtb)" + }, + "widgets_values": [ + "Style" + ] + }, + { + "id": 80, + "type": "PreviewImage", + "pos": [ + 1120, + 190 + ], + "size": [ + 210, + 246 + ], + "flags": {}, + "order": 23, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 163, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 71, + "type": "CLIPTextEncode", + "pos": [ + -610, + 400 + ], + "size": [ + 210, + 75.28300476074219 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 152, + "slot_index": 0 + }, + { + "name": "text", + "type": "STRING", + "link": 151, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + } + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 144 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "Face of a man (looking down), rim lighting, tokyo 1987" + ] + }, + { + "id": 19, + "type": "CLIPSetLastLayer", + "pos": [ + -1980, + 400 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 116 + } + ], + "outputs": [ + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 28, + 29, + 152 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPSetLastLayer" + }, + "widgets_values": [ + -2 + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + -1320, + 330 + ], + "size": [ + 210, + 54 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 28 + }, + { + "name": "text", + "type": "STRING", + "link": 142, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + } + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 4 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "Face of a man (looking down), rim lighting, tokyo 1987" + ] + }, + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + -1320, + 430 + ], + "size": [ + 210, + 54 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 29 + }, + { + "name": "text", + "type": "STRING", + "link": 87, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 6, + 145 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "worst quality, hands, embedding:EasyNegative," + ] + }, + { + "id": 20, + "type": "Styles Loader (mtb)", + "pos": [ + -1650, + 500 + ], + "size": [ + 285.32098388671875, + 78 + ], + "flags": { + "collapsed": false + }, + "order": 4, + "mode": 0, + "outputs": [ + { + "name": "positive", + "type": "STRING", + "links": [], + "shape": 3 + }, + { + "name": "negative", + "type": "STRING", + "links": [ + 87 + ], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "Styles Loader (mtb)" + }, + "widgets_values": [ + "❌Low Token" + ] + }, + { + "id": 81, + "type": "Reroute", + "pos": [ + -1740, + -10 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 164 + } + ], + "outputs": [ + { + "name": "", + "type": "STRING", + "links": [ + 165 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 69, + "type": "String Replace (mtb)", + "pos": [ + -1000, + 0 + ], + "size": [ + 315, + 82 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "string", + "type": "STRING", + "link": 165 + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 151 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String Replace (mtb)" + }, + "widgets_values": [ + "(looking down)", + "(looking up)" + ] + }, + { + "id": 75, + "type": "VAEDecodeTiled", + "pos": [ + 220, + 620 + ], + "size": [ + 210, + 46 + ], + "flags": { + "collapsed": false + }, + "order": 20, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 155, + "slot_index": 0 + }, + { + "name": "vae", + "type": "VAE", + "link": 154, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 158 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecodeTiled" + } + }, + { + "id": 78, + "type": "Film Interpolation (mtb)", + "pos": [ + 1140, + 520 + ], + "size": [ + 315, + 78 + ], + "flags": {}, + "order": 22, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 160, + "slot_index": 0 + }, + { + "name": "film_model", + "type": "FILM_MODEL", + "link": 161, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 162 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Film Interpolation (mtb)" + }, + "widgets_values": [ + 4 + ] + }, + { + "id": 70, + "type": "KSampler", + "pos": [ + -300, + 380 + ], + "size": [ + 315, + 474 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 143 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 144 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 145 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 153, + "slot_index": 3 + }, + { + "name": "seed", + "type": "INT", + "link": 150, + "widget": { + "name": "seed", + "config": [ + "INT", + { + "default": 0, + "min": 0, + "max": 18446744073709552000 + } + ] + }, + "slot_index": 4 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 155 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 542821171532978, + "fixed", + 45, + 8, + "euler_ancestral", + "simple", + 1 + ] + }, + { + "id": 16, + "type": "CheckpointLoaderSimple", + "pos": [ + -2470, + 530 + ], + "size": [ + 315, + 98 + ], + "flags": {}, + "order": 5, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 134 + ], + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 116 + ], + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 117 + ], + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "revAnimated_v122.safetensors" + ] + }, + { + "id": 82, + "type": "Note", + "pos": [ + -398, + 25 + ], + "size": [ + 500.61550625887776, + 270.54209761186075 + ], + "flags": {}, + "order": 6, + "mode": 0, + "properties": { + "text": "THIS LOOKS A BIT MESSY BUT WE BASICALLY USE THE SAME INPUTS (MODEL, SEED etc), ONLY STRING REPLACING THE PROMPT, LOOKING DOWN -> LOOKING UP" + }, + "widgets_values": [ + "THIS LOOKS A BIT MESSY BUT WE BASICALLY USE THE SAME INPUTS (MODEL, SEED etc), ONLY STRING REPLACING THE PROMPT, LOOKING DOWN -> LOOKING UP\n" + ], + "color": "#222", + "bgcolor": "#000" + }, + { + "id": 66, + "type": "VAEDecodeTiled", + "pos": [ + 231, + 403 + ], + "size": [ + 210, + 46 + ], + "flags": { + "collapsed": false + }, + "order": 19, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 138 + }, + { + "name": "vae", + "type": "VAE", + "link": 139, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 157 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecodeTiled" + } + }, + { + "id": 77, + "type": "Concat Images (mtb)", + "pos": [ + 661, + 402 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [ + { + "name": "imageA", + "type": "IMAGE", + "link": 157 + }, + { + "name": "imageB", + "type": "IMAGE", + "link": 158 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 160, + 163 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Concat Images (mtb)" + } + }, + { + "id": 76, + "type": "PreviewImage", + "pos": [ + 1517, + 207 + ], + "size": [ + 511.33429483925056, + 675.8856963258129 + ], + "flags": {}, + "order": 24, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 162 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + } + ], + "links": [ + [ + 2, + 5, + 0, + 3, + 3, + "LATENT" + ], + [ + 4, + 6, + 0, + 3, + 1, + "CONDITIONING" + ], + [ + 6, + 7, + 0, + 3, + 2, + "CONDITIONING" + ], + [ + 28, + 19, + 0, + 6, + 0, + "CLIP" + ], + [ + 29, + 19, + 0, + 7, + 0, + "CLIP" + ], + [ + 87, + 20, + 1, + 7, + 1, + "STRING" + ], + [ + 116, + 16, + 1, + 19, + 0, + "CLIP" + ], + [ + 117, + 16, + 2, + 56, + 0, + "*" + ], + [ + 120, + 57, + 0, + 3, + 0, + "MODEL" + ], + [ + 124, + 56, + 0, + 59, + 0, + "*" + ], + [ + 134, + 16, + 0, + 65, + 0, + "*" + ], + [ + 135, + 65, + 0, + 57, + 0, + "*" + ], + [ + 138, + 3, + 0, + 66, + 0, + "LATENT" + ], + [ + 139, + 59, + 0, + 66, + 1, + "VAE" + ], + [ + 142, + 67, + 0, + 6, + 1, + "STRING" + ], + [ + 143, + 57, + 0, + 70, + 0, + "MODEL" + ], + [ + 144, + 71, + 0, + 70, + 1, + "CONDITIONING" + ], + [ + 145, + 7, + 0, + 70, + 2, + "CONDITIONING" + ], + [ + 148, + 72, + 0, + 3, + 4, + "INT" + ], + [ + 150, + 72, + 0, + 70, + 4, + "INT" + ], + [ + 151, + 69, + 0, + 71, + 1, + "STRING" + ], + [ + 152, + 19, + 0, + 71, + 0, + "CLIP" + ], + [ + 153, + 5, + 0, + 70, + 3, + "LATENT" + ], + [ + 154, + 59, + 0, + 75, + 1, + "VAE" + ], + [ + 155, + 70, + 0, + 75, + 0, + "LATENT" + ], + [ + 157, + 66, + 0, + 77, + 0, + "IMAGE" + ], + [ + 158, + 75, + 0, + 77, + 1, + "IMAGE" + ], + [ + 160, + 77, + 0, + 78, + 0, + "IMAGE" + ], + [ + 161, + 79, + 0, + 78, + 1, + "FILM_MODEL" + ], + [ + 162, + 78, + 0, + 76, + 0, + "IMAGE" + ], + [ + 163, + 77, + 0, + 80, + 0, + "IMAGE" + ], + [ + 164, + 67, + 0, + 81, + 0, + "*" + ], + [ + 165, + 81, + 0, + 69, + 0, + "STRING" + ] + ], + "groups": [ + { + "title": "GENERATE IMAGES", + "bounding": [ + -2477, + -219, + 3018, + 1350 + ], + "color": "#a1309b", + "locked": false + }, + { + "title": "FILM INTERPOLATION", + "bounding": [ + 559, + 66, + 1264, + 832 + ], + "color": "#3f789e", + "locked": false + } + ], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file From b5176ca0ee489ada52b6632f68b794b4f709d5ba Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Tue, 18 Jul 2023 03:56:17 +0200 Subject: [PATCH 15/70] =?UTF-8?q?docs:=20=F0=9F=93=9D=20add=20preview=20fo?= =?UTF-8?q?r=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 examples/README.md diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..86caa4f --- /dev/null +++ b/examples/README.md @@ -0,0 +1,7 @@ +# Examples + +## 01 Faceswap + + +## 02 FILM interpolation + From cbe67edd4befb7260be01fa09af8448e5bcf5680 Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Tue, 18 Jul 2023 03:58:38 +0200 Subject: [PATCH 16/70] =?UTF-8?q?docs:=20=F0=9F=93=9D=20add=20model=20noti?= =?UTF-8?q?ce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 86caa4f..7256e58 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,5 +1,5 @@ # Examples - +All the examples use the [RevAnimated model 1.22](https://civitai.com/models/7371?modelVersionId=46846) ## 01 Faceswap From 46eab5ca2f0e04d872d87c849b11551fd219bdb9 Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Tue, 18 Jul 2023 21:30:42 +0200 Subject: [PATCH 17/70] =?UTF-8?q?docs:=20=F0=9F=93=9D=20add=20more=20descr?= =?UTF-8?q?iption=20to=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/README.md b/examples/README.md index 7256e58..23bf2e8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,7 +1,12 @@ # Examples All the examples use the [RevAnimated model 1.22](https://civitai.com/models/7371?modelVersionId=46846) ## 01 Faceswap + +This example showcase the `Face Swap` & `Restore Face` nodes to replace the character with Georges Lucas's face. +The face reference image is using the `Load Image From Url` node to avoid bundling input images. + ## 02 FILM interpolation +This example showcase the FILM interpolation implementation. Here we do text replacement on the condition of two distinct images sharing the same model, input latent & seed to get relatively close images. From 3c3c4380bd1a3f0eed5216b835e076c26fce2f88 Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Tue, 18 Jul 2023 21:40:26 +0200 Subject: [PATCH 18/70] =?UTF-8?q?docs:=20=F0=9F=93=9D=20fold=20each=20comf?= =?UTF-8?q?y=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INSTALL.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 7d37e56..3ccd5e0 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -9,21 +9,26 @@ ### Dependencies - -#### Custom virtualenv (I use this mainly) - +

Custom Virtualenv (I use this mainly)

+ 1. Make sure you are in the Python environment you use for ComfyUI. 2. Install the required dependencies by running the following command: ```bash pip install -r comfy_mtb/requirements.txt ``` -#### Comfy-portable / standalone (from ComfyUI releases) +
+ +

Comfy-portable / standalone (from ComfyUI releases)

+ If you use the `python-embeded` from ComfyUI standalone then you are not able to pip install dependencies with binaries when they don't have wheels, in this case check the last [release](https://github.com/melMass/comfy_mtb/releases) there is a bundle for linux and windows with prebuilt wheels (only the ones that require building from source), check [this issue (#1)](https://github.com/melMass/comfy_mtb/issues/1) for more info. ![image](https://github.com/melMass/comfy_mtb/assets/7041726/2934fa14-3725-427c-8b9e-2b4f60ba1b7b) -#### Google Colab + +
+ +

Google Colab

Add a new code cell just after the **Run ComfyUI with localtunnel (Recommended Way)** header (before the code cell) ![preview of where to add it on colab](https://github.com/melMass/comfy_mtb/assets/7041726/35df2ef1-14f9-44cd-aa65-353829188cd7) @@ -43,11 +48,12 @@ If after running this, colab complains about needing to restart runtime, do it, > **Note**: -> If you don't need all models, remove the `-y` as collab actually supportd user input: ![image](https://github.com/melMass/comfy_mtb/assets/7041726/40fc3602-f1d4-432a-98fd-ce2240f5ad06) +> If you don't need all models, remove the `-y` as collab actually supports user input: ![image](https://github.com/melMass/comfy_mtb/assets/7041726/40fc3602-f1d4-432a-98fd-ce2240f5ad06) > **Preview** > ![image](https://github.com/melMass/comfy_mtb/assets/7041726/b5b2b2d9-f1e8-4c43-b1db-7dfc5e07be86) +
### Models Download From 0cc54e58ec86c28354cae37e14f39e831c13ea02 Mon Sep 17 00:00:00 2001 From: melMass Date: Thu, 20 Jul 2023 00:00:22 +0200 Subject: [PATCH 19/70] =?UTF-8?q?chore:=20=E2=9C=A8=20before=20categorize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 22 ++++ nodes/graph_utils.py | 34 +++++- nodes/image_interpolation.py | 1 + web/comfy_shared.js | 196 +++++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 web/comfy_shared.js diff --git a/__init__.py b/__init__.py index b780a99..64137cc 100644 --- a/__init__.py +++ b/__init__.py @@ -14,6 +14,8 @@ NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} NODE_CLASS_MAPPINGS_DEBUG = {} +__version__ = "0.1.0" + def extract_nodes_from_source(filename): source_code = "" @@ -135,6 +137,26 @@ log.info( ) ) +# - ENDPOINT +from server import PromptServer +from .log import mklog +from aiohttp import web + +endlog = mklog("endpoint") + + +@PromptServer.instance.routes.get("/mtb-status") +async def get_full_library(request): + files = [] + endlog.debug("Getting status") + return web.json_response( + { + "registered": NODE_CLASS_MAPPINGS_DEBUG, + "failed": failed, + } + ) + + # - WAS Dictionary MANIFEST = { "name": "MTB Nodes", # The title that will be displayed on Node Class menu,. and Node Class view diff --git a/nodes/graph_utils.py b/nodes/graph_utils.py index dabb09c..800c3c4 100644 --- a/nodes/graph_utils.py +++ b/nodes/graph_utils.py @@ -1,6 +1,7 @@ import torch import folder_paths import os +from ..log import log class SaveTensors: @@ -64,6 +65,33 @@ class SaveTensors: return f"{filename_prefix}_{counter:05}" -__nodes__ = [ - SaveTensors, -] +class StringReplace: + """Basic string replacement""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": ("STRING", {"forceInput": True}), + "old": ("STRING", {"default": ""}), + "new": ("STRING", {"default": ""}), + } + } + + FUNCTION = "replace_str" + RETURN_TYPES = ("STRING",) + CATEGORY = "string" + + def replace_str(self, string: str, old: str, new: str): + log.debug(f"Current string: {string}") + log.debug(f"Find string: {old}") + log.debug(f"Replace string: {new}") + + string = string.replace(old, new) + + log.debug(f"New string: {string}") + + return (string,) + + +__nodes__ = [SaveTensors, StringReplace] diff --git a/nodes/image_interpolation.py b/nodes/image_interpolation.py index d74cf63..00c5040 100644 --- a/nodes/image_interpolation.py +++ b/nodes/image_interpolation.py @@ -59,6 +59,7 @@ class GetBatchFromHistory: offset=0, ): if not enable or count == 0: + log.debug("Load from history is disabled for this iteration") return (torch.zeros(0),) frames = [] server_address = "localhost:3000" diff --git a/web/comfy_shared.js b/web/comfy_shared.js new file mode 100644 index 0000000..45f7e98 --- /dev/null +++ b/web/comfy_shared.js @@ -0,0 +1,196 @@ +import { app } from "/scripts/app.js"; + +//- WIDGET UTILS +const CONVERTED_TYPE = "converted-widget"; + +export function offsetDOMWidget(widget, ctx, node, widgetWidth, widgetY, height) { + 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 + widgetY); + + const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) + Object.assign(widget.inputEl.style, { + transformOrigin: "0 0", + transform: scale, + left: `${transform.a + transform.e}px`, + top: `${transform.d + transform.f}px`, + width: `${widgetWidth - (margin * 2)}px`, + height: `${widget.parent.inputHeight - (margin * 2)}px`, + position: "absolute", + background: (!node.color) ? '' : node.color, + color: (!node.color) ? '' : 'white', + zIndex: app.graph._nodes.indexOf(node), + }) +} + +/** + * Extracts the type and link type from a widget config object. + * @param {*} config + * @returns + */ +export function getWidgetType(config) { + // Special handling for COMBO so we restrict links based on the entries + let type = config[0]; + let linkType = type; + if (type instanceof Array) { + type = "COMBO"; + linkType = linkType.join(","); + } + return { type, linkType }; +} + +export const dynamic_connection = (node, index, connected, connectionPrefix = "input_", connectionType = "PSDLAYER") => { + + // remove all non connected inputs + if (!connected && node.inputs.length > 1) { + console.debug(`Removing input ${index} (${node.inputs[index].name})`) + if (node.widgets) { + const w = node.widgets.find((w) => w.name === node.inputs[index].name); + if (w) { + + w.onRemove?.(); + node.widgets.length = node.widgets.length - 1 + } + } + node.removeInput(index) + + // make inputs sequential again + for (let i = 0; i < node.inputs.length; i++) { + node.inputs[i].name = `${connectionPrefix}${i + 1}` + } + } + + // add an extra input + if (node.inputs[node.inputs.length - 1].link != undefined) { + console.debug(`Adding input ${node.inputs.length + 1} (${connectionPrefix}${node.inputs.length + 1})`) + + node.addInput(`${connectionPrefix}${node.inputs.length + 1}`, connectionType) + } + +} + + +/** + * Appends a callback to the extra menu options of a given node type. + * @param {*} nodeType + * @param {*} cb + */ +export function addMenuHandler(nodeType, cb) { + const getOpts = nodeType.prototype.getExtraMenuOptions; + nodeType.prototype.getExtraMenuOptions = function () { + const r = getOpts.apply(this, arguments); + cb.apply(this, arguments); + return r; + }; +} + +export function hideWidget(node, widget, suffix = "") { + widget.origType = widget.type; + widget.hidden = true + 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 + const { link } = node.inputs.find((i) => i.widget?.name === widget.name); + if (link == null) { + 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); + } + } +} + +export function showWidget(widget) { + widget.type = widget.origType; + widget.computeSize = widget.origComputeSize; + widget.serializeValue = widget.origSerializeValue; + + delete widget.origType; + delete widget.origComputeSize; + delete widget.origSerializeValue; + + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + showWidget(w); + } + } +} + +export function convertToWidget(node, widget) { + showWidget(widget); + const sz = node.size; + node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name)); + + 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])]); +} + + +export function convertToInput(node, widget, config) { + hideWidget(node, widget); + + const { linkType } = shared.getWidgetType(config); + + // Add input and store widget config for creating on primitive node + const sz = node.size; + node.addInput(widget.name, linkType, { + widget: { name: widget.name, 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])]); +} + +export function hideWidgetForGood(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 + // const w = node.inputs?.find((i) => i.widget?.name === widget.name); + // if (w?.link == null) { + // 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) { + hideWidgetForGood(node, w, ":" + widget.name); + } + } +} + + +//- COLOR UTILS +export function isColorBright(rgb, threshold = 240) { + const brightess = getBrightness(rgb) + return brightess > threshold +} + +function getBrightness(rgbObj) { + return Math.round(((parseInt(rgbObj[0]) * 299) + (parseInt(rgbObj[1]) * 587) + (parseInt(rgbObj[2]) * 114)) / 1000) +} From d2b396236a10fe620ebebabd5a22c36159921913 Mon Sep 17 00:00:00 2001 From: melMass Date: Thu, 20 Jul 2023 00:26:34 +0200 Subject: [PATCH 20/70] =?UTF-8?q?feat:=20=E2=9C=A8=20categorize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They now all live under mtb/ in the node creation context menu. --- nodes/conditions.py | 6 +-- nodes/crop.py | 21 ++++----- nodes/deep_bump.py | 2 +- nodes/faceenhance.py | 4 +- nodes/faceswap.py | 4 +- nodes/fun.py | 91 ++++++++++++++++++++++++++++++++++-- nodes/graph_utils.py | 6 +-- nodes/image_interpolation.py | 10 ++-- nodes/image_processing.py | 26 +++++------ nodes/latent_processing.py | 2 +- nodes/mask.py | 2 +- 11 files changed, 127 insertions(+), 47 deletions(-) diff --git a/nodes/conditions.py b/nodes/conditions.py index 287fb7a..ffcf268 100644 --- a/nodes/conditions.py +++ b/nodes/conditions.py @@ -35,7 +35,7 @@ class SmartStep: RETURN_TYPES = ("INT", "INT", "INT") RETURN_NAMES = ("step", "start", "end") FUNCTION = "do_step" - CATEGORY = "conditioning" + CATEGORY = "mtb/conditioning" def do_step(self, step, start_percent, end_percent): start = int(step * start_percent / 100) @@ -92,7 +92,7 @@ class StylesLoader: } } - CATEGORY = "conditioning" + CATEGORY = "mtb/conditioning" RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("positive", "negative") @@ -168,7 +168,7 @@ class TextToImage: RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("image",) FUNCTION = "text_to_image" - CATEGORY = "utils" + CATEGORY = "mtb/generate" def text_to_image( self, text, font, wrap, font_size, width, height, color, background diff --git a/nodes/crop.py b/nodes/crop.py index 2aa03f0..09d3dd6 100644 --- a/nodes/crop.py +++ b/nodes/crop.py @@ -6,6 +6,7 @@ import numpy as np class BoundingBox: """The bounding box (BBOX) custom type used by other nodes""" + def __init__(self): pass @@ -28,7 +29,7 @@ class BoundingBox: RETURN_TYPES = ("BBOX",) FUNCTION = "do_crop" - CATEGORY = "image/crop" + CATEGORY = "mtb/crop" def do_crop(self, x, y, width, height): return (x, y, width, height) @@ -36,6 +37,7 @@ class BoundingBox: class BBoxFromMask: """From a mask extract the bounding box""" + def __init__(self): pass @@ -59,10 +61,9 @@ class BBoxFromMask: "image (optional)", ) FUNCTION = "extract_bounding_box" - CATEGORY = "image/crop" + CATEGORY = "mtb/crop" def extract_bounding_box(self, mask: torch.Tensor, image=None): - mask = tensor2pil(mask) alpha_channel = np.array(mask) @@ -92,6 +93,7 @@ class Crop: The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type The BBOX input takes precedence over the tuple input """ + def __init__(self): pass @@ -120,12 +122,11 @@ class Crop: RETURN_TYPES = ("IMAGE", "MASK", "BBOX") FUNCTION = "do_crop" - CATEGORY = "image/crop" + CATEGORY = "mtb/crop" def do_crop( self, image: torch.Tensor, mask=None, x=0, y=0, width=256, height=256, bbox=None ): - image = image.numpy() if mask: mask = mask.numpy() @@ -149,6 +150,7 @@ class Uncrop: The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type The BBOX input takes precedence over the tuple input""" + def __init__(self): pass @@ -169,7 +171,7 @@ class Uncrop: RETURN_TYPES = ("IMAGE",) FUNCTION = "do_crop" - CATEGORY = "image/crop" + CATEGORY = "mtb/crop" def do_crop(self, image, crop_image, bbox, border_blending): def inset_border(image, border_width=20, border_color=(0)): @@ -214,9 +216,4 @@ class Uncrop: return (pil2tensor(image.convert("RGB")),) -__nodes__ = [ - BBoxFromMask, - BoundingBox, - Crop, - Uncrop -] \ No newline at end of file +__nodes__ = [BBoxFromMask, BoundingBox, Crop, Uncrop] diff --git a/nodes/deep_bump.py b/nodes/deep_bump.py index f9ece06..639aaa9 100644 --- a/nodes/deep_bump.py +++ b/nodes/deep_bump.py @@ -271,7 +271,7 @@ class DeepBump: RETURN_TYPES = ("IMAGE",) FUNCTION = "apply" - CATEGORY = "image processing" + CATEGORY = "mtb/textures" def apply( self, diff --git a/nodes/faceenhance.py b/nodes/faceenhance.py index 00c33c0..4865413 100644 --- a/nodes/faceenhance.py +++ b/nodes/faceenhance.py @@ -52,7 +52,7 @@ class LoadFaceEnhanceModel: RETURN_TYPES = ("FACEENHANCE_MODEL",) RETURN_NAMES = ("model",) FUNCTION = "load_model" - CATEGORY = "face" + CATEGORY = "mtb/facetools" def load_model(self, model_name, upscale=2, bg_upsampler=None): basic = "RestoreFormer" not in model_name @@ -127,7 +127,7 @@ class RestoreFace: RETURN_TYPES = ("IMAGE",) FUNCTION = "restore" - CATEGORY = "face" + CATEGORY = "mtb/facetools" @classmethod def INPUT_TYPES(cls): diff --git a/nodes/faceswap.py b/nodes/faceswap.py index 0a154ba..aa1fc25 100644 --- a/nodes/faceswap.py +++ b/nodes/faceswap.py @@ -46,7 +46,7 @@ class LoadFaceSwapModel: RETURN_TYPES = ("FACESWAP_MODEL",) FUNCTION = "load_model" - CATEGORY = "face" + CATEGORY = "mtb/facetools" def load_model(self, faceswap_model: str): model_path = os.path.join( @@ -88,7 +88,7 @@ class FaceSwap: RETURN_TYPES = ("IMAGE",) FUNCTION = "swap" - CATEGORY = "face" + CATEGORY = "mtb/facetools" def swap( self, diff --git a/nodes/fun.py b/nodes/fun.py index a53341e..4835931 100644 --- a/nodes/fun.py +++ b/nodes/fun.py @@ -1,6 +1,86 @@ import qrcode -from ..utils import pil2tensor -from PIL import Image +from ..utils import pil2tensor, here, tensor2np +from PIL import Image, ImageOps +from functools import lru_cache +import numpy as np +import torch +import hashlib +from pathlib import Path +from ..log import log +import uuid +import folder_paths +import comfy.model_management as model_management +import subprocess + + +class SaveGif: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "fps": ("INT", {"default": 12, "min": 1, "max": 120}), + "resize_by": ("FLOAT", {"default": 1.0, "min": 0.1}), + "pingpong": ("BOOL", {"default": False}), + } + } + + RETURN_TYPES = () + OUTPUT_NODE = True + CATEGORY = "mtb/IO" + FUNCTION = "save_gif" + + def save_gif(self, image, fps=12, resize_by=1.0, pingpong=False): + if image.size(0) == 0: + return ("",) + + images = tensor2np(image) + images = [frame.astype(np.uint8) for frame in images] + if pingpong: + reversed_frames = images[::-1] + images.extend(reversed_frames) + + height, width, _ = image[0].shape + + ruuid = uuid.uuid4() + out_path = f"{folder_paths.output_directory}/{ruuid}.gif" + + log.debug(f"Saving a gif file {width}x{height} as {ruuid}.gif") + + # Prepare the FFmpeg command + command = [ + "ffmpeg", + "-y", + "-f", + "rawvideo", + "-vcodec", + "rawvideo", + "-s", + f"{width}x{height}", + "-pix_fmt", + "rgb24", # GIF only supports rgb24 + "-r", + str(fps), + "-i", + "-", + "-vf", + f"fps={fps},scale={width * resize_by}:-1", # Set frame rate and resize if necessary + "-y", + out_path, + ] + + process = subprocess.Popen(command, stdin=subprocess.PIPE) + + for frame in images: + model_management.throw_exception_if_processing_interrupted() + process.stdin.write(frame.tobytes()) + + process.stdin.close() + process.wait() + results = [] + results.append({"filename": f"{ruuid}.gif", "subfolder": "", "type": "output"}) + return {"ui": {"images": results}} + class QrCode: @@ -31,7 +111,7 @@ class QrCode: RETURN_TYPES = ("IMAGE",) FUNCTION = "do_qr" - CATEGORY = "fun" + CATEGORY = "mtb/generate" def do_qr(self, url, width, height, error_correct, box_size, border, invert): if error_correct == "L" or error_correct not in ["M", "Q", "H"]: @@ -63,4 +143,7 @@ class QrCode: return (pil2tensor(code),) -__nodes__ = [QrCode] +__nodes__ = [ + QrCode, + SaveGif, +] diff --git a/nodes/graph_utils.py b/nodes/graph_utils.py index 800c3c4..18db1de 100644 --- a/nodes/graph_utils.py +++ b/nodes/graph_utils.py @@ -9,7 +9,7 @@ class SaveTensors: def __init__(self): self.output_dir = folder_paths.get_output_directory() - self.type = "output" + self.type = "mtb/debug" @classmethod def INPUT_TYPES(cls): @@ -27,7 +27,7 @@ class SaveTensors: FUNCTION = "save" OUTPUT_NODE = True RETURN_TYPES = () - CATEGORY = "utils" + CATEGORY = "mtb/debug" def save( self, @@ -80,7 +80,7 @@ class StringReplace: FUNCTION = "replace_str" RETURN_TYPES = ("STRING",) - CATEGORY = "string" + CATEGORY = "mtb/string" def replace_str(self, string: str, old: str, new: str): log.debug(f"Current string: {string}") diff --git a/nodes/image_interpolation.py b/nodes/image_interpolation.py index 00c5040..5ac882f 100644 --- a/nodes/image_interpolation.py +++ b/nodes/image_interpolation.py @@ -49,7 +49,7 @@ class GetBatchFromHistory: RETURN_TYPES = ("IMAGE",) RETURN_NAMES = "images" - CATEGORY = "animation" + CATEGORY = "mtb/animation" FUNCTION = "load_from_history" def load_from_history( @@ -129,7 +129,7 @@ class LoadFilmModel: RETURN_TYPES = ("FILM_MODEL",) FUNCTION = "load_model" - CATEGORY = "face" + CATEGORY = "mtb/frame iterpolation" def load_model(self, film_model: str): model_path = Path(folder_paths.models_dir) / "FILM" / film_model @@ -163,7 +163,7 @@ class FilmInterpolation: RETURN_TYPES = ("IMAGE",) FUNCTION = "do_interpolation" - CATEGORY = "animation" + CATEGORY = "mtb/frame iterpolation" def do_interpolation( self, @@ -218,7 +218,7 @@ class ConcatImages: RETURN_TYPES = ("IMAGE",) FUNCTION = "concat_images" - CATEGORY = "animation" + CATEGORY = "mtb/image" @classmethod def INPUT_TYPES(cls): @@ -270,7 +270,7 @@ class ExportToProres: RETURN_TYPES = ("VIDEO",) OUTPUT_NODE = True FUNCTION = "export_prores" - CATEGORY = "animation" + CATEGORY = "mtb/IO" def export_prores( self, diff --git a/nodes/image_processing.py b/nodes/image_processing.py index 394523a..4b3d333 100644 --- a/nodes/image_processing.py +++ b/nodes/image_processing.py @@ -68,7 +68,7 @@ class ColorCorrect: RETURN_TYPES = ("IMAGE",) FUNCTION = "correct" - CATEGORY = "image/postprocessing" + CATEGORY = "mtb/image processing" @staticmethod def gamma_correction_tensor(image, gamma): @@ -198,7 +198,7 @@ class HsvToRgb: RETURN_TYPES = ("IMAGE",) FUNCTION = "convert" - CATEGORY = "image/postprocessing" + CATEGORY = "mtb/image processing" def convert(self, image): image = image.numpy() @@ -228,7 +228,7 @@ class RgbToHsv: RETURN_TYPES = ("IMAGE",) FUNCTION = "convert" - CATEGORY = "image/postprocessing" + CATEGORY = "mtb/image processing" def convert(self, image): image = image.numpy() @@ -261,7 +261,7 @@ class ImageCompare: RETURN_TYPES = ("IMAGE",) FUNCTION = "compare" - CATEGORY = "image" + CATEGORY = "mtb/image" def compare(self, imageA: torch.Tensor, imageB: torch.Tensor, mode): imageA = imageA.numpy() @@ -297,7 +297,7 @@ class LoadImageFromUrl: RETURN_TYPES = ("IMAGE",) FUNCTION = "load" - CATEGORY = "image" + CATEGORY = "mtb/IO" def load(self, url): # get the image from the url @@ -325,7 +325,7 @@ class Denoise: RETURN_TYPES = ("IMAGE",) FUNCTION = "denoise" - CATEGORY = "image/postprocessing" + CATEGORY = "mtb/image processing" def denoise(self, image: torch.Tensor, weight): image = image.numpy() @@ -360,7 +360,7 @@ class Blur: RETURN_TYPES = ("IMAGE",) FUNCTION = "blur" - CATEGORY = "image/postprocessing" + CATEGORY = "mtb/image processing" def blur(self, image: torch.Tensor, sigmaX, sigmaY): image = image.numpy() @@ -387,7 +387,7 @@ class DeglazeImage: def INPUT_TYPES(cls): return {"required": {"image": ("IMAGE",)}} - CATEGORY = "image" + CATEGORY = "mtb/image processing" RETURN_TYPES = ("IMAGE",) FUNCTION = "deglaze_image" @@ -412,7 +412,7 @@ class MaskToImage: } } - CATEGORY = "image/mask" + CATEGORY = "mtb/generate" RETURN_TYPES = ("IMAGE",) @@ -453,7 +453,7 @@ class ColoredImage: } } - CATEGORY = "image" + CATEGORY = "mtb/generate" RETURN_TYPES = ("IMAGE",) @@ -483,7 +483,7 @@ class ImagePremultiply: } } - CATEGORY = "image" + CATEGORY = "mtb/image" RETURN_TYPES = ("IMAGE",) FUNCTION = "premultiply" @@ -531,7 +531,7 @@ class ImageResizeFactor: }, } - CATEGORY = "image" + CATEGORY = "mtb/image" RETURN_TYPES = ("IMAGE", "MASK") FUNCTION = "resize" @@ -651,7 +651,7 @@ class SaveImageGrid: OUTPUT_NODE = True - CATEGORY = "image" + CATEGORY = "mtb/IO" def create_image_grid(self, image_list): total_images = len(image_list) diff --git a/nodes/latent_processing.py b/nodes/latent_processing.py index 93d35dd..63e3ad1 100644 --- a/nodes/latent_processing.py +++ b/nodes/latent_processing.py @@ -18,7 +18,7 @@ class LatentLerp: RETURN_TYPES = ("LATENT",) FUNCTION = "lerp_latent" - CATEGORY = "latent" + CATEGORY = "mtb/latent" def lerp_latent(self, A, B, t): a = A.copy() diff --git a/nodes/mask.py b/nodes/mask.py index fac0ac0..f7f76ab 100644 --- a/nodes/mask.py +++ b/nodes/mask.py @@ -52,7 +52,7 @@ class ImageRemoveBackgroundRembg: "Image", ) FUNCTION = "remove_background" - CATEGORY = "image" + CATEGORY = "mtb/image" # bgcolor: Optional[Tuple[int, int, int, int]] def remove_background( From 95afbdbf76e66897e632252d876384ada9acf153 Mon Sep 17 00:00:00 2001 From: melMass Date: Fri, 21 Jul 2023 16:33:46 +0200 Subject: [PATCH 21/70] =?UTF-8?q?feat:=20=F0=9F=94=A5=20add=20API=20endpoi?= =?UTF-8?q?nts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 62 ++- endpoint.py | 39 ++ html/style.css | 52 +++ web/mtb_widgets.js | 913 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1065 insertions(+), 1 deletion(-) create mode 100644 endpoint.py create mode 100644 html/style.css create mode 100644 web/mtb_widgets.js diff --git a/__init__.py b/__init__.py index 64137cc..4660d6a 100644 --- a/__init__.py +++ b/__init__.py @@ -141,11 +141,12 @@ log.info( from server import PromptServer from .log import mklog from aiohttp import web +from importlib import reload endlog = mklog("endpoint") -@PromptServer.instance.routes.get("/mtb-status") +@PromptServer.instance.routes.get("/mtb/status") async def get_full_library(request): files = [] endlog.debug("Getting status") @@ -157,6 +158,65 @@ async def get_full_library(request): ) +@PromptServer.instance.routes.post("/mtb/debug") +async def set_debug(request): + json_data = await request.json() + enabled = json_data.get("enabled") + if enabled: + os.environ["MTB_DEBUG"] = "true" + else: + if "MTB_DEBUG" in os.environ: + # del os.environ["MTB_DEBUG"] + os.environ.pop("MTB_DEBUG") + return web.json_response({"message": f"Debug mode {'set' if enabled else 'unset'}"}) + + +@PromptServer.instance.routes.get("/mtb") +async def get_home(request): + from . import endpoint + + reload(endpoint) + # Check if the request prefers HTML content + if "text/html" in request.headers.get("Accept", ""): + # # Return an HTML page + html_response = f""" + + """ + return web.Response( + text=endpoint.render_base_template("MTB", html_response), + content_type="text/html", + ) + + # Return JSON for other requests + return web.json_response({"message": "Welcome to MTB!"}) + + +@PromptServer.instance.routes.get("/mtb/debug") +async def get_debug(request): + from . import endpoint + + reload(endpoint) + enabled = False + if "MTB_DEBUG" in os.environ: + enabled = True + # Check if the request prefers HTML content + if "text/html" in request.headers.get("Accept", ""): + # # Return an HTML page + html_response = f""" +

MTB Debug Status: {'Enabled' if enabled else 'Disabled'}

+ """ + return web.Response( + text=endpoint.render_base_template("Debug", html_response), + content_type="text/html", + ) + + # Return JSON for other requests + return web.json_response({"enabled": enabled}) + + # - WAS Dictionary MANIFEST = { "name": "MTB Nodes", # The title that will be displayed on Node Class menu,. and Node Class view diff --git a/endpoint.py b/endpoint.py new file mode 100644 index 0000000..1da1315 --- /dev/null +++ b/endpoint.py @@ -0,0 +1,39 @@ +from .utils import here + + +def render_base_template(title, content): + css_content = "" + css_path = here / "html" / "style.css" + if css_path: + with open(css_path, "r") as css_file: + css_content = css_file.read() + + github_icon_svg = """""" + return f""" + + + + {title} + + + +
+ Comfy MTB Logo + Comfy MTB + + {github_icon_svg} + +
+ +
+ {content} +
+ +
+ +
+ + + """ diff --git a/html/style.css b/html/style.css new file mode 100644 index 0000000..9c10a3e --- /dev/null +++ b/html/style.css @@ -0,0 +1,52 @@ +html { + height: 100%; + margin: 0; + padding: 0; + background-color: rgb(33, 33, 33); + color: whitesmoke; +} + +body { + margin: 0; + padding: 0; + font-family: monospace; + height: 100%; + background-color: rgb(33, 33, 33); + +} + +.title { + font-size: 2.5em; + font-weight: 700; + margin: 1em; +} + +header { + display: flex; + align-items: center; + vertical-align: middle; + justify-content: space-between; + background-color: rgb(12, 12, 12); + padding: 1em; + margin: 0; +} + +main { + display: flex; + align-items: center; + vertical-align: middle; + justify-content: center; + padding: 1em; + margin: 0; + height: 80%; +} + +.flex-container { + display: flex; + flex-direction: column; +} + +.menu { + font-size: 3em; + text-align: center; +} \ No newline at end of file diff --git a/web/mtb_widgets.js b/web/mtb_widgets.js new file mode 100644 index 0000000..ee62ec6 --- /dev/null +++ b/web/mtb_widgets.js @@ -0,0 +1,913 @@ +import { app } from "/scripts/app.js"; +import parseCss from '/extensions/mtb/extern/parse-css.js' +import * as shared from '/extensions/mtb/comfy_shared.js' + +import { api } from "/scripts/api.js"; + +import { ComfyWidgets } from "/scripts/widgets.js"; + +const newTypes = ["BOOL", "COLOR", "BBOX"] + +const bboxWidget = (key, val) => { + /** @type {import("./types/litegraph").IWidget} */ + const widget = { + name: key, + type: "BBOX", + // options: val, + y: 0, + value: val || [0, 0, 0, 0], + options: {}, + + draw: function (ctx, + node, + widget_width, + widgetY, + height) { + const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5 + + const show_text = true + const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; + const background_color = LiteGraph.WIDGET_BGCOLOR; + const text_color = LiteGraph.WIDGET_TEXT_COLOR; + const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; + const H = LiteGraph.NODE_WIDGET_HEIGHT; + + + var margin = 15; + if (hide) return; + + ctx.textAlign = "left"; + ctx.strokeStyle = outline_color; + ctx.fillStyle = background_color; + ctx.beginPath(); + if (show_text) + ctx.roundRect(margin, widgetY, widget_width - margin * 2, H, [H * 0.5]); + else + ctx.rect(margin, widgetY, widget_width - margin * 2, H); + ctx.fill(); + if (show_text) { + if (!this.disabled) + ctx.stroke(); + ctx.fillStyle = text_color; + if (!this.disabled) { + ctx.beginPath(); + ctx.moveTo(margin + 16, widgetY + 5); + ctx.lineTo(margin + 6, widgetY + H * 0.5); + ctx.lineTo(margin + 16, widgetY + H - 5); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(widget_width - margin - 16, widgetY + 5); + ctx.lineTo(widget_width - margin - 6, widgetY + H * 0.5); + ctx.lineTo(widget_width - margin - 16, widgetY + H - 5); + ctx.fill(); + } + ctx.fillStyle = secondary_text_color; + ctx.fillText(this.label || this.name, margin * 2 + 5, widgetY + H * 0.7); + ctx.fillStyle = text_color; + ctx.textAlign = "right"; + + ctx.fillText( + Number(this.value).toFixed( + this.options?.precision !== undefined + ? this.options.precision + : 3 + ), + widget_width - margin * 2 - 20, + widgetY + H * 0.7 + ); + + } + }, + mouse: function (event, pos, node) { + var old_value = this.value; + var x = pos[0] - node.pos[0]; + var y = pos[1] - node.pos[1]; + var width = node.size[0]; + + if (event.type == LiteGraph.pointerevents_method + "move" && this.type == "BBOX") { + if (event.deltaX) + this.value += event.deltaX * 0.1 * (this.options?.step || 1); + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min; + } + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max; + } + } else if (event.type == LiteGraph.pointerevents_method + "down") { + var values = this.options?.values; + if (values && values.constructor === Function) { + values = this.options.values(w, node); + } + var values_list = null; + + var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; + if (this.type == "BBOX") { + this.value += delta * 0.1 * (this.options.step || 1); + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min; + } + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max; + } + } else if (delta) { //clicked in arrow, used for combos + var index = -1; + this.last_mouseclick = 0; //avoids dobl click event + if (values.constructor === Object) + index = values_list.indexOf(String(this.value)) + delta; + else + index = values_list.indexOf(this.value) + delta; + if (index >= values_list.length) { + index = values_list.length - 1; + } + if (index < 0) { + index = 0; + } + if (values.constructor === Array) + this.value = values[index]; + else + this.value = index; + } else { //combo clicked + var text_values = values != values_list ? Object.values(values) : values; + var menu = new LiteGraph.ContextMenu(text_values, { + scale: Math.max(1, this.ds.scale), + event: event, + className: "dark", + callback: inner_clicked.bind(w) + }, + ref_window); + function inner_clicked(v, option, event) { + if (values != values_list) + v = text_values.indexOf(v); + this.value = v; + shared.inner_value_change(this, v, event); + app.canvas.setDirty(true) + return false; + } + } + } //end mousedown + else if (event.type == LiteGraph.pointerevents_method + "up" && this.type == "BBOX") { + var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; + if (event.click_time < 200 && delta == 0) { + this.prompt("Value", this.value, function (v) { + // check if v is a valid equation or a number + if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { + try {//solve the equation if possible + v = eval(v); + } catch (e) { } + } + this.value = Number(v); + shared.inner_value_change(this, this.value, event); + }.bind(w), + event); + } + } + + if (old_value != this.value) + setTimeout( + function () { + shared.inner_value_change(this, this.value, event); + }.bind(this), + 20 + ); + + app.canvas.setDirty(true) + + }, + computeSize: function (width) { + return [width, 32]; + }, + // onDrawBackground: function (ctx) { + // if (!this.flags.collapsed) return; + // this.inputEl.style.display = "block"; + // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; + // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; + // }, + // onInputChange: function (e) { + // const property = e.target.dataset.property; + // const bbox = this.getInputData(0); + // if (!bbox) return; + // bbox[property] = parseFloat(e.target.value); + // this.setOutputData(0, bbox); + // } + } + + widget.desc = "Represents a Bounding Box with x, y, width, and height."; + return widget + +} +const bboxWidgetDOM = (key, val) => { + /** @type {import("./types/litegraph").IWidget} */ + const widget = { + name: key, + type: "BBOX", + // options: val, + y: 0, + value: val || [0, 0, 0, 0], + + draw: function (ctx, + node, + widgetWidth, + widgetY, + height) { + const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5 + this.inputEl.style.display = hide ? "none" : "block"; + if (hide) return; + + shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) + }, + computeSize: function (width) { + return [width, 32]; + }, + // onDrawBackground: function (ctx) { + // if (!this.flags.collapsed) return; + // this.inputEl.style.display = "block"; + // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; + // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; + // }, + onInputChange: function (e) { + const property = e.target.dataset.property; + const bbox = this.getInputData(0); + if (!bbox) return; + bbox[property] = parseFloat(e.target.value); + this.setOutputData(0, bbox); + } + } + widget.inputEl = document.createElement("div") + widget.parent = this + widget.inputEl.innerHTML = ` +
+
+
+
+ `; + // set the class document wide if not present + + shared.defineClass("bbox-input", `background-color: var(--comfy-input-bg); + color: var(--input-text); + overflow: hidden; + width:100%; + overflow-y: auto; + padding: 2px; + resize: none; + border: none; + box-sizing: border-box; + font-size: 10px;`) + + + const bboxInputs = widget.inputEl.querySelectorAll(".bbox-input"); + bboxInputs.forEach((input) => { + input.addEventListener("change", widget.onInputChange.bind(this)); + }); + + widget.desc = "Represents a Bounding Box with x, y, width, and height."; + + document.body.appendChild(widget.inputEl); + + + console.log(widget.inputEl) + return widget + +} +/** + * @returns {import("./types/litegraph").IWidget} widget + */ +const boolWidget = (key, val, compute = false) => { + /** @type {import("/types/litegraph").IWidget} */ + const widget = { + name: key, + type: "BOOL", + options: { default: false }, + y: 0, + value: val || false, + draw: function (ctx, + node, + widgetWidth, + widgetY, + height) { + const hide = this.type !== "BOOL" + if (hide) { + return + } + const border = 6; + const w = 20; + ctx.fillStyle = "black"; + + ctx.fillRect(24, widgetY, w, w); + if (this.value) { + ctx.fillStyle = "white"; + ctx.fillRect(24 + border, widgetY + border, w - border * 2, height - border * 2); + } + console.log(typeof (this.value)) + // write the input name + // choose the fill based on the luminoisty of this.value color + + ctx.fillStyle = "white"; + + + ctx.font = "14px Arial"; + ctx.textAlign = "center"; + ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14); + + }, + get value() { + + return this.inputEl.value === "true"; + }, + set value(x) { + this.inputEl.value = x; + }, + computeSize: function (width) { + return [width, 32]; + }, + mouse: function (e, pos, node) { + if (e.type === "pointerdown") { + // get widgets of type type : "COLOR" + const widgets = node.widgets.filter(w => w.type === "BOOL"); + + for (const w of widgets) { + // color picker + const rect = [w.last_y, w.last_y + 32]; + if (pos[1] > rect[0] && pos[1] < rect[1]) { + console.log("bool", this.value) + // picker.style.position = "absolute"; + // picker.style.left = ( pos[0]) + "px"; + // picker.style.top = ( pos[1]) + "px"; + + // place at screen center + // picker.style.position = "absolute"; + // picker.style.left = (window.innerWidth / 2) + "px"; + // picker.style.top = (window.innerHeight / 2) + "px"; + // picker.style.transform = "translate(-50%, -50%)"; + // picker.style.zIndex = 1000; + + this.value = this.value ? "false" : "true" + + } + } + } + } + } + + // create a checkbox + widget.inputEl = document.createElement("input") + widget.inputEl.type = "checkbox" + widget.inputEl.value = false + document.body.appendChild(widget.inputEl); + return widget + +} + +/** + * @returns {import("./types/litegraph").IWidget} widget + */ +const colorWidget = (key, val, compute = false) => { + /** @type {import("/types/litegraph").IWidget} */ + const widget = {} + widget.y = 0 + widget.name = key; + widget.type = "COLOR"; + widget.options = { default: "#ff0000" }; + widget.value = val || "#ff0000"; + widget.draw = function (ctx, + node, + widgetWidth, + widgetY, + height) { + const hide = this.type !== "COLOR" && app.canvas.ds.scale > 0.5 + if (hide) { + return + } + + const border = 3; + // draw a rect with a border and a fill color + ctx.fillStyle = "#000"; + ctx.fillRect(0, widgetY, widgetWidth, height); + ctx.fillStyle = this.value; + ctx.fillRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); + // write the input name + // choose the fill based on the luminoisty of this.value color + const color = parseCss(this.value.default || this.value) + if (!color) { + return + } + ctx.fillStyle = shared.isColorBright(color.values, 125) ? "#000" : "#fff"; + + + ctx.font = "14px Arial"; + ctx.textAlign = "center"; + ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14); + + + + // ctx.strokeStyle = "#fff"; + // ctx.strokeRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); + + + // ctx.fillStyle = "#000"; + // ctx.fillRect(widgetWidth/2 - border / 2 , widgetY + border / 2 , widgetWidth/2 + border / 2, height + border / 2); + // ctx.fillStyle = this.value; + // ctx.fillRect(widgetWidth/2, widgetY, widgetWidth/2, height); + + } + widget.mouse = function (e, pos, node) { + if (e.type === "pointerdown") { + // get widgets of type type : "COLOR" + const widgets = node.widgets.filter(w => w.type === "COLOR"); + + for (const w of widgets) { + // color picker + const rect = [w.last_y, w.last_y + 32]; + if (pos[1] > rect[0] && pos[1] < rect[1]) { + console.log("color picker", node) + const picker = document.createElement("input"); + picker.type = "color"; + picker.value = this.value; + // picker.style.position = "absolute"; + // picker.style.left = ( pos[0]) + "px"; + // picker.style.top = ( pos[1]) + "px"; + + // place at screen center + picker.style.position = "absolute"; + picker.style.left = "999999px"//(window.innerWidth / 2) + "px"; + picker.style.top = "999999px" //(window.innerHeight / 2) + "px"; + // picker.style.transform = "translate(-50%, -50%)"; + // picker.style.zIndex = 1000; + + + + document.body.appendChild(picker); + + picker.addEventListener("change", () => { + this.value = picker.value; + node.graph._version++; + node.setDirtyCanvas(true, true); + picker.remove(); + }); + + picker.click() + + } + } + } + } + widget.computeSize = function (width) { + return [width, 32]; + } + + return widget; +} + + +// VIDEO: (node, inputName, inputData, app) => { +// console.log("video") +// const videoWidget = { +// name: "VideoWidget", +// description: "Video Player Widget", +// value: inputData, +// properties: {}, +// widget: null, + +// init: function () { +// this.widget = document.createElement("video"); +// this.widget.width = 200; +// this.widget.height = 120; +// this.widget.controls = true; +// this.widget.style.width = "100%"; +// this.widget.style.height = "100%"; +// this.widget.style.objectFit = "contain"; +// this.widget.style.backgroundColor = "black"; +// this.widget.style.pointerEvents = "none"; +// node.addWidget(inputName, videoWidget.widget, inputData); +// }, + +// setValue: function (value, options) { +// if (value instanceof HTMLVideoElement) { +// this.widget.src = value.src; +// } else if (typeof value === "string") { +// this.widget.src = value; +// } +// }, + +// getValue: function () { +// return this.widget.src; +// }, + +// append: function (parent) { +// parent.appendChild(this.widget); +// }, + +// remove: function () { +// this.widget.parentNode.removeChild(this.widget); +// } +// }; +// return { +// widget: videoWidget, +// } +// } + + + +/** + * @returns {import("./types/comfy").ComfyExtension} extension + */ +const mtb_widgets = { + name: "mtb.widgets", + + init: () => { }, + + + /** + * @param {import("./types/comfy").NodeType} nodeType + * @param {import("./types/comfy").NodeDef} nodeData + * @param {import("./types/comfy").App} app + */ + async beforeRegisterNodeDef(nodeType, nodeData, app) { + + const rinputs = nodeData.input?.required; + + let has_custom = false + if (nodeData.input && nodeData.input.required) { + for (const i of Object.keys(nodeData.input.required)) { + const input_type = nodeData.input.required[i][0] + + if (newTypes.includes(input_type)) { + has_custom = true + break; + } + } + } + if (has_custom) { + + //- Add widgets on node creation + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + this.serialize_widgets = true; + for (const [key, input] of Object.entries(rinputs)) { + switch (input[0]) { + case "COLOR": + const colW = colorWidget(key, input[1]) + this.addCustomWidget(colW) + const associated_input = this.inputs.findIndex((i) => i.widget?.name === key); + if (associated_input !== -1) { + this.inputs[associated_input].widget = colW + } + + + + break; + case "BOOL": + // const widg = boolWidget(key, input[1]) + // this.addCustomWidget(widg) + this.addWidget("toggle", key, false, function (value, widget, node) { + console.log(value) + + }) + //this.removeInput(this.inputs.findIndex((i) => i.widget?.name === key)); + + break; + case "BBOX": + + // const x_widget = ComfyWidgets.FLOAT(this, `${key}_x`, [undefined, { default: 1, min: 1, max: Number.MAX_SAFE_INTEGER }]).widget; + // const y_widget = ComfyWidgets.FLOAT(this, `${key}_y`, [undefined, { default: 1, min: 1, max: Number.MAX_SAFE_INTEGER }]).widget; + // const width_widget = ComfyWidgets.FLOAT(this, `${key}_width`, [undefined, { default: 1, min: 1, max: Number.MAX_SAFE_INTEGER }]).widget; + // const height_widget = ComfyWidgets.FLOAT(this, `${key}_height`, [undefined, { default: 1, min: 1, max: Number.MAX_SAFE_INTEGER }]).widget; + + + + // this.addWidget("number", `${key}_x`, false, function (value, widget, node) { }) + // this.addWidget("number", `${key}_y`, false, function (value, widget, node) { }) + // this.addWidget("number", `${key}_width`, false, function (value, widget, node) { }) + // this.addWidget("number", `${key}_height`, false, function (value, widget, node) { }) + + const bboxW = bboxWidget(key, input[1]) + this.addCustomWidget(bboxW) + break; + default: + break + } + + + // } + } + + this.setSize?.(this.computeSize()) + + this.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].canvas) { + this.widgets[y].canvas.remove(); + } + } + }; + } + + //- On initial configure of nodes hide all converted widgets + const origOnConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined; + console.log(this) + + if (this.inputs) { + for (const input of this.inputs) { + console.log(input) + if (input.widget) { + // if (newTypes.includes(input.type)) { + + console.log(input.widget) + if (input.widget.hidden) { + console.debug(`Already hidden skipping ${input.widget.name}`) + continue + } + const w = this.widgets.find((w) => w.name === input.widget.name); + if (w) { + console.log(`hidding ${w.name} from ${this.label}`) + shared.hideWidget(this, w); + } else { + console.log(`converting to widget ${w}`) + + shared.convertToWidget(this, input) + } + } + } + } + + return r; + }; + + + //- Extra menus + const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; + nodeType.prototype.getExtraMenuOptions = function (_, options) { + const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : undefined; + if (this.widgets) { + let toInput = []; + let toWidget = []; + for (const w of this.widgets) { + if (w.type === shared.CONVERTED_TYPE) { + toWidget.push({ + content: `Convert ${w.name} to widget`, + callback: () => shared.convertToWidget(this, w), + }); + } else { + const config = nodeData?.input?.required[w.name] || nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}]; + + toInput.push({ + content: `Convert ${w.name} to input`, + callback: () => shared.convertToInput(this, w, config), + }); + } + } + if (toInput.length) { + options.push(...toInput, null); + } + + if (toWidget.length) { + options.push(...toWidget, null); + } + } + + return r; + }; + + } + + + + // if (nodeData.input && nodeData.input.includes("BOOL")) { + + // addMenuHandler(nodeType, function (_, options) { + // options.unshift({ + // content: "convert bool input to widget", + // callback: () => { + + // console.log("Nahh not yet") + // }, + // }); + // }); + + // } + + //- Extending Python Nodes + switch (nodeData.name) { + case "Psd Save (mtb)": { + // const onConnectionsChange = nodeType.prototype.onConnectionsChange; + nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) { + // const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined; + shared.dynamic_connection(this, index, connected) + } + break + } + case "Animation Builder (mtb)": { + // console.log(nodeType.prototype) + + + + nodeType.prototype.onNodeCreated = function () { + + this.changeMode(LiteGraph.ALWAYS) + // api.addEventListener("executed", ({ detail }) => { + + // console.log("executed", detail) + // console.log(this) + + // }) + const raw_iteration = this.widgets.find((w) => w.name === "raw_iteration"); + const raw_loop = this.widgets.find((w) => w.name === "raw_loop"); + + + const total_frames = this.widgets.find((w) => w.name === "total_frames"); + const loop_count = this.widgets.find((w) => w.name === "loop_count"); + + shared.hideWidgetForGood(this, raw_iteration); + shared.hideWidgetForGood(this, raw_loop); + + raw_iteration._value = 0 + // Object.defineProperty(raw_iteration, "value", { + // get() { + // return this._value + // }, + // set(value) { + // this._value = value; + // }, + // }); + + const value_preview = ComfyWidgets["STRING"](this, "PREVIEW_raw_iteration", ["STRING", { multiline: true }], app).widget; + value_preview.inputEl.readOnly = true; + value_preview.inputEl.disabled = true; + + + // value_preview.inputEl.style.opacity = 0.6; + value_preview.inputEl.style.textAlign = "center"; + value_preview.inputEl.style.fontSize = "2.5em"; + value_preview.inputEl.style.backgroundColor = "black"; + + value_preview.inputEl.style.setProperty("--comfy-input-bg", "transparent"); + value_preview.inputEl.style.setProperty("background", "red", "important"); + // remove the comfy-multiline-input class + + // disallow user selection + value_preview.inputEl.style.userSelect = "none"; + + const loop_preview = ComfyWidgets["STRING"](this, "PREVIEW_raw_iteration", ["STRING", { multiline: true }], app).widget; + loop_preview.inputEl.readOnly = true; + loop_preview.inputEl.disabled = true; + + + // loop_preview.inputEl.style.opacity = 0.6; + loop_preview.inputEl.style.textAlign = "center"; + loop_preview.inputEl.style.fontSize = "1.5em"; + loop_preview.inputEl.style.backgroundColor = "black"; + + loop_preview.inputEl.style.setProperty("--comfy-input-bg", "transparent"); + loop_preview.inputEl.style.setProperty("background", "red", "important"); + // remove the comfy-multiline-input class + + // disallow user selection + loop_preview.inputEl.style.userSelect = "none"; + + const onReset = () => { + raw_iteration.value = 0; + raw_loop.value = 0; + + value_preview.value = 0; + loop_preview.value = 0; + + app.canvas.setDirty(true); + } + + const reset_button = this.addWidget("button", `Reset`, "reset", onReset); + + const run_button = this.addWidget("button", `Queue`, "queue", () => { + onReset() // this could maybe be a setting or checkbox + app.queuePrompt(0, total_frames.value * loop_count.value) + + }); + + + + raw_iteration.afterQueued = function () { + this.value++; + raw_loop.value = Math.floor(this.value / total_frames.value); + value_preview.value = `raw: ${this.value} +frame: ${this.value % total_frames.value}`; + if (raw_loop.value + 1 > loop_count.value) { + loop_preview.value = `Done 😎!` + return + } + + loop_preview.value = `current loop: ${raw_loop.value + 1}/${loop_count.value}` + + } + // ComfyWidgets.hideWidget() + // if (raw_iteration > 0) { + // raw_iteration.inputEl.readOnly = true + // raw_iteration.inputEl.style.opacity = 0.6; + // console.log(raw_iteration) + // raw_iteration.onRemove?.(); + // console.log(this.widgets[raw_iteration].type); + + // raw_iteration.afterQueued = function () { + // this.value++ + // } + // } + + + + + } + //LiteGraph.ALWAYS + const onExecuted = nodeType.prototype.onExecuted; + + nodeType.prototype.onExecuted = function (data) { + console.log("executed") + onExecuted?.apply(this, data) + if (this.widgets) { + const pos = this.widgets.findIndex((w) => w.name === "preview"); + if (pos !== -1) { + for (let i = pos; i < this.widgets.length; i++) { + this.widgets[i].onRemove?.(); + } + this.widgets.length = pos; + } + } + + const w = ComfyWidgets["STRING"](this, "preview", ["STRING", { multiline: true }], app).widget; + w.inputEl.readOnly = true; + w.inputEl.style.opacity = 0.6; + w.value = data.total_frames; + + // this.onResize?.(this.size); + this.setSize?.(this.computeSize()) + + } + // const onAfterExecuteNode = nodeType.prototype.onAfterExecuteNode; + // nodeType.prototype.onAfterExecuteNode = function () { + // onAfterExecuteNode?.apply(this) + // console.log("after", this) + + // } + console.debug(`Registered ${nodeType.name} node extra events`) + break + + } + + case "Debug (mtb)": { + // console.log(`Registered ${nodeType.name} node extra events`) + // nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) { + // // const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined; + // console.debug({ + // type, index, connected, link_info + // }) + + // shared.dynamic_connection(this, index, connected, "anything_", "*") + // } + + // const onNodeCreated = nodeType.prototype.onNodeCreated; + // nodeType.prototype.onNodeCreated = function () { + // const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + + // this.addInput(`input_${this.inputs.length + 1}`, "*") + + // } + + break + + } + default: { + break + + } + + } + // const onNodeCreated = nodeType.prototype.onNodeCreated; + + // nodeType.prototype.onNodeCreated = function () { + // const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + + // } + + + + + // console.log(nodeData.output) + // if (nodeData.output.includes("VIDEO") && nodeData.output_node) { + // console.log(`Found video output for ${nodeType}`) + // console.log(nodeData) + + // } + + // if (nodeData.name === "Psd Save (mtb)") { + // console.log(`Found psd node`) + // console.log(nodeData) + + // } + + + + } +}; + + +app.registerExtension(mtb_widgets); \ No newline at end of file From aa551ebe57801c69010815119fe21e19a858780c Mon Sep 17 00:00:00 2001 From: melMass Date: Fri, 21 Jul 2023 23:36:37 +0200 Subject: [PATCH 22/70] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20FitNumber=20nod?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit quite useful in conjunction to AnimationBuilder --- nodes/graph_utils.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/nodes/graph_utils.py b/nodes/graph_utils.py index 18db1de..e514c42 100644 --- a/nodes/graph_utils.py +++ b/nodes/graph_utils.py @@ -94,4 +94,44 @@ class StringReplace: return (string,) -__nodes__ = [SaveTensors, StringReplace] +class FitNumber: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 0, "forceInput": True}), + "clamp": ("BOOL", {"default": False}), + "source_min": ("FLOAT", {"default": 0.0}), + "source_max": ("FLOAT", {"default": 1.0}), + "target_min": ("FLOAT", {"default": 0.0}), + "target_max": ("FLOAT", {"default": 1.0}), + } + } + + FUNCTION = "set_range" + RETURN_TYPES = ("FLOAT",) + CATEGORY = "mtb/math" + + def set_range( + self, + value: float, + clamp: bool, + source_min: float, + source_max: float, + target_min: float, + target_max: float, + ): + res = target_min + (target_max - target_min) * (value - source_min) / ( + source_max - source_min + ) + + if clamp: + if target_min > target_max: + res = max(min(res, target_min), target_max) + else: + res = max(min(res, target_max), target_min) + + return (res,) + + +__nodes__ = [SaveTensors, StringReplace, FitNumber] From 8a59508ff91d6b2d9ca287ef1c054ec5755337a4 Mon Sep 17 00:00:00 2001 From: melMass Date: Fri, 21 Jul 2023 23:37:12 +0200 Subject: [PATCH 23/70] =?UTF-8?q?fix:=20=F0=9F=90=9B=20tensor2pil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils.py b/utils.py index cafe31d..9bc7788 100644 --- a/utils.py +++ b/utils.py @@ -53,7 +53,8 @@ def tensor2pil(image: torch.Tensor) -> List[Image.Image]: if batch_count > 1: out = [] - out.extend([tensor2pil(image[i]) for i in range(batch_count)]) + for i in range(batch_count): + out.extend(tensor2pil(image[i])) return out return [ From 67d3783ac9186da6bba4b7dc7e8dc3d5db5a1b0f Mon Sep 17 00:00:00 2001 From: melMass Date: Fri, 21 Jul 2023 23:38:19 +0200 Subject: [PATCH 24/70] =?UTF-8?q?fix:=20=F0=9F=90=9B=20crop=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/crop.py | 160 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 127 insertions(+), 33 deletions(-) diff --git a/nodes/crop.py b/nodes/crop.py index 09d3dd6..96bd71e 100644 --- a/nodes/crop.py +++ b/nodes/crop.py @@ -1,10 +1,25 @@ import torch -from ..utils import tensor2pil, pil2tensor -from PIL import Image, ImageFilter, ImageDraw +from ..utils import tensor2pil, pil2tensor, tensor2np, np2tensor +from PIL import Image, ImageFilter, ImageDraw, ImageChops import numpy as np +from ..log import log -class BoundingBox: + +class BoolInput: + @classmethod + def INPUT_TYPES(cls): + return {"required": {"bool": ("BOOL",)}} + + RETURN_TYPES = ("BOOL",) + FUNCTION = "do_bool" + CATEGORY = "mtb/test" + + def do_bool(self, bool): + return (bool,) + + +class Bbox: """The bounding box (BBOX) custom type used by other nodes""" def __init__(self): @@ -14,6 +29,7 @@ class BoundingBox: def INPUT_TYPES(cls): return { "required": { + # "bbox": ("BBOX",), "x": ("INT", {"default": 0, "max": 10000000, "min": 0, "step": 1}), "y": ("INT", {"default": 0, "max": 10000000, "min": 0, "step": 1}), "width": ( @@ -31,11 +47,12 @@ class BoundingBox: FUNCTION = "do_crop" CATEGORY = "mtb/crop" - def do_crop(self, x, y, width, height): + def do_crop(self, x, y, width, height): # bbox return (x, y, width, height) + # return bbox -class BBoxFromMask: +class BboxFromMask: """From a mask extract the bounding box""" def __init__(self): @@ -64,9 +81,22 @@ class BBoxFromMask: CATEGORY = "mtb/crop" def extract_bounding_box(self, mask: torch.Tensor, image=None): - mask = tensor2pil(mask) + # if image != None: + # if mask.size(0) != image.size(0): + # if mask.size(0) != 1: + # log.error( + # f"Batch count mismatch for mask and image, it can either be 1 mask for X images, or X masks for X images (mask: {mask.shape} | image: {image.shape})" + # ) + + # raise Exception( + # f"Batch count mismatch for mask and image, it can either be 1 mask for X images, or X masks for X images (mask: {mask.shape} | image: {image.shape})" + # ) + + _mask = tensor2pil(1.0 - mask)[0] + + # we invert it + alpha_channel = np.array(_mask) - alpha_channel = np.array(mask) non_zero_indices = np.nonzero(alpha_channel) min_x, max_x = np.min(non_zero_indices[1]), np.max(non_zero_indices[1]) @@ -75,11 +105,16 @@ class BBoxFromMask: # Create a bounding box tuple if image != None: # Convert the image to a NumPy array - image = image.numpy() - # Crop the image from the bounding box - image = image[:, min_y:max_y, min_x:max_x] - image = torch.from_numpy(image) + imgs = tensor2np(image) + out = [] + for img in imgs: + # Crop the image from the bounding box + img = img[min_y:max_y, min_x:max_x, :] + log.debug(f"Cropped image to shape {img.shape}") + out.append(img) + image = np2tensor(out) + log.debug(f"Cropped images shape: {image.shape}") bounding_box = (min_x, min_y, max_x - min_x, max_y - min_y) return ( bounding_box, @@ -145,6 +180,38 @@ class Crop: ) +# def calculate_intersection(rect1, rect2): +# x_left = max(rect1[0], rect2[0]) +# y_top = max(rect1[1], rect2[1]) +# x_right = min(rect1[2], rect2[2]) +# y_bottom = min(rect1[3], rect2[3]) + +# return (x_left, y_top, x_right, y_bottom) + + +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]), + ) + if new_bbox != bbox: + log.warn(f"BBox too big, constrained to {new_bbox}") + + return new_bbox + + +def bbox_to_region(bbox, target_size=None): + bbox = bbox_check(bbox, target_size) + + # to region + return (bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3]) + + class Uncrop: """Uncrops an image to a given bounding box @@ -184,36 +251,63 @@ class Uncrop: ) return bordered_image - image = tensor2pil(image) - crop_img = tensor2pil(crop_image) - crop_img = crop_img.convert("RGB") + single = image.size(0) == 1 + if image.size(0) != crop_image.size(0): + if not single: + raise ValueError( + "The Image batch count is greater than 1, but doesn't match the crop_image batch count. If using batches they should either match or only crop_image must be greater than 1" + ) - # uncrop the image based on the bounding box - bb_x, bb_y, bb_width, bb_height = bbox + images = tensor2pil(image) + crop_imgs = tensor2pil(crop_image) + out_images = [] + for i, crop in enumerate(crop_imgs): + if single: + img = images[0] + else: + img = images[i] - if border_blending > 1.0: - border_blending = 1.0 - elif border_blending < 0.0: - border_blending = 0.0 + # uncrop the image based on the bounding box + bb_x, bb_y, bb_width, bb_height = bbox - blend_ratio = (max(crop_img.size) / 2) * float(border_blending) + paste_region = bbox_to_region((bb_x, bb_y, bb_width, bb_height), img.size) + # log.debug(f"Paste region: {paste_region}") + # new_region = adjust_paste_region(img.size, paste_region) + # log.debug(f"Adjusted paste region: {new_region}") + # # Check if the adjusted paste region is different from the original - blend = image.convert("RGBA") - mask = Image.new("L", image.size, 0) + crop_img = crop.convert("RGB") - mask_block = Image.new("L", (bb_width, bb_height), 255) - mask_block = inset_border(mask_block, int(blend_ratio / 2), (0)) + log.debug(f"Crop image size: {crop_img.size}") + log.debug(f"Image size: {img.size}") - mask.paste(mask_block, (bb_x, bb_y, bb_x + bb_width, bb_y + bb_height)) - blend.paste(crop_img, (bb_x, bb_y, bb_x + bb_width, bb_y + bb_height)) + if border_blending > 1.0: + border_blending = 1.0 + elif border_blending < 0.0: + border_blending = 0.0 - mask = mask.filter(ImageFilter.BoxBlur(radius=blend_ratio / 4)) - mask = mask.filter(ImageFilter.GaussianBlur(radius=blend_ratio / 4)) + blend_ratio = (max(crop_img.size) / 2) * float(border_blending) - blend.putalpha(mask) - image = Image.alpha_composite(image.convert("RGBA"), blend) + blend = img.convert("RGBA") + mask = Image.new("L", img.size, 0) - return (pil2tensor(image.convert("RGB")),) + mask_block = Image.new("L", (bb_width, bb_height), 255) + mask_block = inset_border(mask_block, int(blend_ratio / 2), (0)) + + mask.paste(mask_block, paste_region) + log.debug(f"Blend size: {blend.size} | kind {blend.mode}") + log.debug(f"Crop image size: {crop_img.size} | kind {crop_img.mode}") + log.debug(f"BBox: {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),) -__nodes__ = [BBoxFromMask, BoundingBox, Crop, Uncrop] +__nodes__ = [BboxFromMask, Bbox, Crop, Uncrop, BoolInput] From b27b8ef91fe7335b1df3766547edaf4b9625ae4d Mon Sep 17 00:00:00 2001 From: melMass Date: Fri, 21 Jul 2023 23:39:20 +0200 Subject: [PATCH 25/70] =?UTF-8?q?feat:=20=F0=9F=9A=80=20debug=20node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Still needs some work, especially in widget drawing, but already useful as is, so it will do for now --- nodes/debug.py | 54 ++++++++++++++ web/debug.js | 187 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 nodes/debug.py create mode 100644 web/debug.js diff --git a/nodes/debug.py b/nodes/debug.py new file mode 100644 index 0000000..75f1fe2 --- /dev/null +++ b/nodes/debug.py @@ -0,0 +1,54 @@ +from ..utils import tensor2pil +from ..log import log +import io, base64 +import torch + + +class Debug: + @classmethod + def INPUT_TYPES(cls): + return { + "required": {"anything_1": ("*")}, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "do_debug" + CATEGORY = "mtb/debug" + OUTPUT_NODE = True + + def do_debug(self, **kwargs): + output = { + "ui": {"b64_images": [], "text": []}, + "result": ("A"), + } + for k, v in kwargs.items(): + anything = v + text = "" + if isinstance(anything, torch.Tensor): + log.debug(f"Tensor: {anything.shape}") + + # write the images to temp + + image = tensor2pil(anything) + b64_imgs = [] + for im in image: + buffered = io.BytesIO() + im.save(buffered, format="JPEG") + b64_imgs.append( + base64.b64encode(buffered.getvalue()).decode("utf-8") + ) + + output["ui"]["b64_images"] += b64_imgs + log.debug(f"Input {k} contains {len(b64_imgs)} images") + elif isinstance(anything, bool): + log.debug(f"Input {k} contains boolean: {anything}") + output["ui"]["text"] += ["True" if anything else "False"] + else: + text = str(anything) + log.debug(f"Input {k} contains text: {text}") + output["ui"]["text"] += [text] + + return output + + +__nodes__ = [Debug] diff --git a/web/debug.js b/web/debug.js new file mode 100644 index 0000000..49cfd44 --- /dev/null +++ b/web/debug.js @@ -0,0 +1,187 @@ +import { app } from "/scripts/app.js"; +import * as shared from '/extensions/mtb/comfy_shared.js' +import { log } from '/extensions/mtb/comfy_shared.js' + +// TODO: respect inputs order... + + +const DEBUG_WIDGETS = { + "image": (val, index) => { + const w = { + name: `anything_${index}`, + type: "image", + value: val, + draw: function (ctx, + node, + widgetWidth, + widgetY, + height) { + const [cw, ch] = this.computeSize(widgetWidth) + shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) + }, + computeSize: function (width) { + const ratio = this.inputRatio || 1; + if (width) { + return [width, width / ratio + 4] + } + return [128, 128] + }, + onRemove: function () { + if (this.inputEl) { + this.inputEl.remove(); + } + } + } + + w.inputEl = document.createElement("img"); + w.inputEl.src = "data:image/jpeg;base64," + w.value; + w.inputEl.onload = function () { + w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight; + } + document.body.appendChild(w.inputEl); + return w + }, + "text": (val, index) => { + const w = { + name: `anything_${index}`, + type: "debug_text", + val: val, + draw: function (ctx, + node, + widgetWidth, + widgetY, + height) { + // const [cw, ch] = this.computeSize(widgetWidth) + shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) + }, + computeSize: function (width) { + const value = this.inputEl.innerHTML + if (!value) { + return [32, 32] + } + if (!width) { + log(`No width ${this.parent.size}`) + } + + const fontSize = 25; // Assuming 1rem = 16px + + const oldFont = app.ctx.font + app.ctx.font = `${fontSize}px Arial`; + + const words = value.split(" "); + const lines = []; + let currentLine = ""; + for (const word of words) { + const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}`; + + const testWidth = app.ctx.measureText(testLine).width; + + // log(`Testing line ${testLine}, width: ${testWidth}, width: ${width}, ratio: ${testWidth / width}`) + if (testWidth > width) { + lines.push(currentLine); + currentLine = word; + } else { + currentLine = testLine; + } + } + app.ctx.font = oldFont; + lines.push(currentLine); + + // Step 3: Calculate the widget width and height + const textHeight = lines.length * (fontSize + 2); // You can adjust the line height (2 in this case) + const maxLineWidth = lines.reduce((maxWidth, line) => Math.max(maxWidth, app.ctx.measureText(line).width), 0); + const widgetWidth = Math.max(width || this.width || 32, maxLineWidth); + const widgetHeight = textHeight + 10; // Additional padding for spacing + return [widgetWidth, widgetHeight + 4] + + }, + onRemove: function () { + if (this.inputEl) { + this.inputEl.remove(); + } + + } + } + w.inputEl = document.createElement("p"); + w.inputEl.style.textAlign = "center"; + w.inputEl.style.fontSize = "1.5em"; + w.inputEl.style.color = "var(--input-text)"; + w.inputEl.style.fontFamily = "monospace"; + w.inputEl.innerHTML = val + document.body.appendChild(w.inputEl); + + return w + } +} + +app.registerExtension({ + name: "mtb.Debug", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "Debug (mtb)") { + const onConnectionsChange = nodeType.prototype.onConnectionsChange; + nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) { + const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined; + // TODO: remove all widgets on disconnect once computed + shared.dynamic_connection(this, index, connected, "anything_", "*") + + //- infer type + if (link_info) { + const fromNode = this.graph._nodes.find((otherNode) => otherNode.id == link_info.origin_id); + const type = fromNode.outputs[link_info.origin_slot].type; + this.inputs[index].type = type; + // this.inputs[index].label = type.toLowerCase() + } + //- restore dynamic input + if (!connected) { + this.inputs[index].type = "*"; + this.inputs[index].label = `anything_${index + 1}` + } + } + + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + log(message) + onExecuted?.apply(this, arguments); + log(message) + if (this.widgets) { + // const pos = this.widgets.findIndex((w) => w.name === "anything_1"); + // if (pos !== -1) { + for (let i = 0; i < this.widgets.length; i++) { + this.widgets[i].onRemove?.(); + } + this.widgets.length = 0; + + } + let widgetI = 1 + if (message.text) { + for (const txt of message.text) { + const w = this.addCustomWidget(DEBUG_WIDGETS["text"](txt, widgetI)) + w.parent = this; + widgetI++; + } + } + if (message.b64_images) { + for (const img of message.b64_images) { + const w = this.addCustomWidget(DEBUG_WIDGETS["image"](img, widgetI)) + w.parent = this; + widgetI++; + } + // this.onResize?.(this.size); + // this.resize?.(this.size) + this.setSize(this.computeSize()) + }; + + this.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].canvas) { + this.widgets[y].canvas.remove(); + } + this.widgets[y].onRemove?.(); + } + } + } + } + } +} +); From e480d071171cffa789930620f1e7ccc76473bf93 Mon Sep 17 00:00:00 2001 From: melMass Date: Fri, 21 Jul 2023 23:40:08 +0200 Subject: [PATCH 26/70] =?UTF-8?q?refactor:=20=F0=9F=9A=A7=20remove=20color?= =?UTF-8?q?=5Fwidget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widgets registration will be done in a single extension --- web/color_widget.js | 194 -------------------------------------------- 1 file changed, 194 deletions(-) delete mode 100644 web/color_widget.js diff --git a/web/color_widget.js b/web/color_widget.js deleted file mode 100644 index c598599..0000000 --- a/web/color_widget.js +++ /dev/null @@ -1,194 +0,0 @@ -// Define the Color Picker widget class -import parseCss from '/extensions/mtb/extern/parse-css.js' -import { app } from "/scripts/app.js"; -import { ComfyWidgets } from "/scripts/widgets.js"; - -export function CUSTOM_INT(node, inputName, val, func, config = {}) { - return { - widget: node.addWidget( - "number", - inputName, - val, - func, - Object.assign({}, { min: 0, max: 4096, step: 640, precision: 0 }, config) - ), - }; -} -const dumb_call = (v, d, node) => { - console.log("dumb_call", { v, d, node }); -} -function isColorBright(rgb, threshold = 240) { - const brightess = getBrightness(rgb) - - return brightess > threshold -} - -function getBrightness(rgbObj) { - return Math.round(((parseInt(rgbObj[0]) * 299) + (parseInt(rgbObj[1]) * 587) + (parseInt(rgbObj[2]) * 114)) / 1000) -} - - -/** - * @returns {import("./types/litegraph").IWidget} widget - */ -const custom = (key, val, compute = false) => { - /** @type {import("/types/litegraph").IWidget} */ - const widget = {} - // widget.y = 0; - widget.name = key; - widget.type = "COLOR"; - widget.options = { default: "#ff0000" }; - widget.value = val || "#ff0000"; - widget.draw = function (ctx, - node, - widgetWidth, - widgetY, - height) { - const border = 3; - - // draw a rect with a border and a fill color - ctx.fillStyle = "#000"; - ctx.fillRect(0, widgetY, widgetWidth, height); - ctx.fillStyle = this.value; - ctx.fillRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); - // write the input name - // choose the fill based on the luminoisty of this.value color - const color = parseCss(this.value.default || this.value) - if (!color) { - return - } - ctx.fillStyle = isColorBright(color.values, 125) ? "#000" : "#fff"; - - - ctx.font = "14px Arial"; - ctx.textAlign = "center"; - ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14); - - - - // ctx.strokeStyle = "#fff"; - // ctx.strokeRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); - - - // ctx.fillStyle = "#000"; - // ctx.fillRect(widgetWidth/2 - border / 2 , widgetY + border / 2 , widgetWidth/2 + border / 2, height + border / 2); - // ctx.fillStyle = this.value; - // ctx.fillRect(widgetWidth/2, widgetY, widgetWidth/2, height); - - } - widget.mouse = function (e, pos, node) { - if (e.type === "pointerdown") { - // get widgets of type type : "COLOR" - const widgets = node.widgets.filter(w => w.type === "COLOR"); - - for (const w of widgets) { - // color picker - const rect = [w.last_y, w.last_y + 32]; - if (pos[1] > rect[0] && pos[1] < rect[1]) { - console.log("color picker", node) - const picker = document.createElement("input"); - picker.type = "color"; - picker.value = this.value; - // picker.style.position = "absolute"; - // picker.style.left = ( pos[0]) + "px"; - // picker.style.top = ( pos[1]) + "px"; - - // place at screen center - // picker.style.position = "absolute"; - // picker.style.left = (window.innerWidth / 2) + "px"; - // picker.style.top = (window.innerHeight / 2) + "px"; - // picker.style.transform = "translate(-50%, -50%)"; - // picker.style.zIndex = 1000; - - - - document.body.appendChild(picker); - - picker.addEventListener("change", () => { - this.value = picker.value; - node.graph._version++; - node.setDirtyCanvas(true, true); - document.body.removeChild(picker); - }); - - // simulate click with screen center - const pointer_event = new MouseEvent('click', { - bubbles: false, - // cancelable: true, - pointerType: "mouse", - clientX: window.innerWidth / 2, - clientY: window.innerHeight / 2, - x: window.innerWidth / 2, - y: window.innerHeight / 2, - offsetX: window.innerWidth / 2, - offsetY: window.innerHeight / 2, - screenX: window.innerWidth / 2, - screenY: window.innerHeight / 2, - - - }); - console.log(e) - picker.dispatchEvent(pointer_event); - - } - } - } - } - widget.computeSize = function (width) { - return [width, 32]; - } - - return widget; -} - -app.registerExtension({ - name: "mtb.ColorPicker", - - async beforeRegisterNodeDef(nodeType, nodeData, app) { - - //console.log("mtb.ColorPicker", { nodeType, nodeData, app }); - const rinputs = nodeData.input?.required; // object with key/value pairs, "0" is the type - // console.log(nodeData.name, { nodeType, nodeData, app }); - - if (!rinputs) return; - - - let has_color = false; - for (const [key, input] of Object.entries(rinputs)) { - if (input[0] === "COLOR") { - has_color = true; - // input[1] = { default: "#ff0000" }; - - } - } - - if (!has_color) return; - - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; - this.serialize_widgets = true; - // if (rinputs[0] === "COLOR") { - // console.log(nodeData.name, { nodeType, nodeData, app }); - - // loop through the inputs to find the color inputs - for (const [key, input] of Object.entries(rinputs)) { - if (input[0] === "COLOR") { - let widget = custom(key, input[1]) - - this.addCustomWidget(widget) - } - // } - } - - this.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].canvas) { - this.widgets[y].canvas.remove(); - } - } - }; - } - } -}); From 91bb95da914468de533b040324700c7f9707e4fb Mon Sep 17 00:00:00 2001 From: melMass Date: Fri, 21 Jul 2023 23:40:51 +0200 Subject: [PATCH 27/70] =?UTF-8?q?feat:=20=E2=9C=A8comfy=5Fwidget=20shared?= =?UTF-8?q?=20utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/comfy_shared.js | 100 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 7 deletions(-) diff --git a/web/comfy_shared.js b/web/comfy_shared.js index 45f7e98..f6a2232 100644 --- a/web/comfy_shared.js +++ b/web/comfy_shared.js @@ -1,7 +1,14 @@ import { app } from "/scripts/app.js"; +export const log = (...args) => { + if (window.MTB_DEBUG) { + console.debug(...args); + } + +} + //- WIDGET UTILS -const CONVERTED_TYPE = "converted-widget"; +export const CONVERTED_TYPE = "converted-widget"; export function offsetDOMWidget(widget, ctx, node, widgetWidth, widgetY, height) { const margin = 10; @@ -18,7 +25,9 @@ export function offsetDOMWidget(widget, ctx, node, widgetWidth, widgetY, height) left: `${transform.a + transform.e}px`, top: `${transform.d + transform.f}px`, width: `${widgetWidth - (margin * 2)}px`, - height: `${widget.parent.inputHeight - (margin * 2)}px`, + // height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`, + height: `${(height || widget.parent?.inputHeight || 32) - (margin * 2)}px`, + position: "absolute", background: (!node.color) ? '' : node.color, color: (!node.color) ? '' : 'white', @@ -46,11 +55,10 @@ export const dynamic_connection = (node, index, connected, connectionPrefix = "i // remove all non connected inputs if (!connected && node.inputs.length > 1) { - console.debug(`Removing input ${index} (${node.inputs[index].name})`) + log(`Removing input ${index} (${node.inputs[index].name})`) if (node.widgets) { const w = node.widgets.find((w) => w.name === node.inputs[index].name); if (w) { - w.onRemove?.(); node.widgets.length = node.widgets.length - 1 } @@ -59,13 +67,13 @@ export const dynamic_connection = (node, index, connected, connectionPrefix = "i // make inputs sequential again for (let i = 0; i < node.inputs.length; i++) { - node.inputs[i].name = `${connectionPrefix}${i + 1}` + node.inputs[i].label = `${connectionPrefix}${i + 1}` } } // add an extra input if (node.inputs[node.inputs.length - 1].link != undefined) { - console.debug(`Adding input ${node.inputs.length + 1} (${connectionPrefix}${node.inputs.length + 1})`) + log(`Adding input ${node.inputs.length + 1} (${connectionPrefix}${node.inputs.length + 1})`) node.addInput(`${connectionPrefix}${node.inputs.length + 1}`, connectionType) } @@ -145,7 +153,7 @@ export function convertToWidget(node, widget) { export function convertToInput(node, widget, config) { hideWidget(node, widget); - const { linkType } = shared.getWidgetType(config); + const { linkType } = getWidgetType(config); // Add input and store widget config for creating on primitive node const sz = node.size; @@ -184,6 +192,50 @@ export function hideWidgetForGood(node, widget, suffix = "") { } } +export function fixWidgets(node) { + if (node.inputs) { + for (const input of node.inputs) { + log(input) + if (input.widget || node.widgets) { + // if (newTypes.includes(input.type)) { + const matching_widget = node.widgets.find((w) => w.name === input.name); + if (matching_widget) { + + + // if (matching_widget.hidden) { + // log(`Already hidden skipping ${matching_widget.name}`) + // continue + // } + const w = node.widgets.find((w) => w.name === matching_widget.name); + if (w && w.type != CONVERTED_TYPE) { + log(w) + log(`hidding ${w.name}(${w.type}) from ${node.type}`) + log(node) + hideWidget(node, w); + } else { + log(`converting to widget ${w}`) + + convertToWidget(node, input) + } + } + } + } + } +} +export function inner_value_change(widget, value, event = undefined) { + if (widget.type == "number" || widget.type == "BBOX") { + value = Number(value); + } else if (widget.type == "BOOL") { + value = Boolean(value) + } + widget.value = value; + if (widget.options && widget.options.property && node.properties[widget.options.property] !== undefined) { + node.setProperty(widget.options.property, value); + } + if (widget.callback) { + widget.callback(widget.value, app.canvas, node, pos, event); + } +} //- COLOR UTILS export function isColorBright(rgb, threshold = 240) { @@ -194,3 +246,37 @@ export function isColorBright(rgb, threshold = 240) { function getBrightness(rgbObj) { return Math.round(((parseInt(rgbObj[0]) * 299) + (parseInt(rgbObj[1]) * 587) + (parseInt(rgbObj[2]) * 114)) / 1000) } + +//- HTML / CSS UTILS +export function defineClass(className, classStyles) { + const styleSheets = document.styleSheets; + + // Helper function to check if the class exists in a style sheet + function classExistsInStyleSheet(styleSheet) { + const rules = styleSheet.rules || styleSheet.cssRules; + for (const rule of rules) { + if (rule.selectorText === `.${className}`) { + return true; + } + } + return false; + } + + // Check if the class is already defined in any of the style sheets + let classExists = false; + for (const styleSheet of styleSheets) { + if (classExistsInStyleSheet(styleSheet)) { + classExists = true; + break; + } + } + + // If the class doesn't exist, add the new class definition to the first style sheet + if (!classExists) { + if (styleSheets[0].insertRule) { + styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0); + } else if (styleSheets[0].addRule) { + styleSheets[0].addRule(`.${className}`, classStyles, 0); + } + } +} From 9aa934f70ff6adf91efb26aa8e5cb21ec575196a Mon Sep 17 00:00:00 2001 From: melMass Date: Fri, 21 Jul 2023 23:43:46 +0200 Subject: [PATCH 28/70] =?UTF-8?q?fix:=20=F0=9F=94=A5=20deprecate=20some=20?= =?UTF-8?q?nodes=20and=20fix=20image=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/faceenhance.py | 2 +- nodes/image_processing.py | 172 +++++++++++--------------------------- nodes/mask.py | 49 +++++++---- nodes/number.py | 6 +- nodes/video.py | 4 +- 5 files changed, 85 insertions(+), 148 deletions(-) diff --git a/nodes/faceenhance.py b/nodes/faceenhance.py index 4865413..e2317af 100644 --- a/nodes/faceenhance.py +++ b/nodes/faceenhance.py @@ -44,7 +44,7 @@ class LoadFaceEnhanceModel: [x.name for x in cls.get_models()], {"default": "None"}, ), - "upscale": ("INT", {"default": 2}), + "upscale": ("INT", {"default": 1}), }, "optional": {"bg_upsampler": ("UPSCALE_MODEL", {"default": None})}, } diff --git a/nodes/image_processing.py b/nodes/image_processing.py index 4b3d333..ad302be 100644 --- a/nodes/image_processing.py +++ b/nodes/image_processing.py @@ -90,19 +90,21 @@ class ColorCorrect: @staticmethod def hsv_adjustment(image: torch.Tensor, hue, saturation, value): - image = tensor2pil(image) - hsv_image = image.convert("HSV") + images = tensor2pil(image) + out = [] + for img in images: + hsv_image = img.convert("HSV") - h, s, v = hsv_image.split() + h, s, v = hsv_image.split() - h = h.point(lambda x: (x + hue * 255) % 256) - s = s.point(lambda x: int(x * saturation)) - v = v.point(lambda x: int(x * value)) + h = h.point(lambda x: (x + hue * 255) % 256) + s = s.point(lambda x: int(x * saturation)) + v = v.point(lambda x: int(x * value)) - hsv_image = Image.merge("HSV", (h, s, v)) - rgb_image = hsv_image.convert("RGB") - - return pil2tensor(rgb_image) + hsv_image = Image.merge("HSV", (h, s, v)) + rgb_image = hsv_image.convert("RGB") + out.append(rgb_image) + return pil2tensor(out) @staticmethod def hsv_adjustment_tensor_not_working(image: torch.Tensor, hue, saturation, value): @@ -182,64 +184,6 @@ class ColorCorrect: return (image,) -class HsvToRgb: - """Convert HSV image to RGB""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "convert" - CATEGORY = "mtb/image processing" - - def convert(self, image): - image = image.numpy() - - image = image.squeeze() - # image = image.transpose(1,2,3,0) - image = hsv2rgb(image) - image = np.expand_dims(image, axis=0) - - # image = image.transpose(3,0,1,2) - return (torch.from_numpy(image),) - - -class RgbToHsv: - """Convert RGB image to HSV""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "convert" - CATEGORY = "mtb/image processing" - - def convert(self, image): - image = image.numpy() - - image = np.squeeze(image) - image = rgb2hsv(image) - image = np.expand_dims(image, axis=0) - - return (torch.from_numpy(image),) - - class ImageCompare: """Compare two images and return a difference image""" @@ -305,37 +249,6 @@ class LoadImageFromUrl: return (pil2tensor(image),) -class Denoise: - """Denoise an image using total variation minimization.""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "weight": ( - "FLOAT", - {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}, - ), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "denoise" - CATEGORY = "mtb/image processing" - - def denoise(self, image: torch.Tensor, weight): - image = image.numpy() - image = image.squeeze() - image = denoise_tv_chambolle(image, weight=weight) - - image = np.expand_dims(image, axis=0) - return (torch.from_numpy(image),) - - class Blur: """Blur an image using a Gaussian filter.""" @@ -371,29 +284,29 @@ class Blur: # https://github.com/lllyasviel/AdverseCleaner/blob/main/clean.py -def deglaze_np_img(np_img): - y = np_img.copy() - for _ in range(64): - y = cv2.bilateralFilter(y, 5, 8, 8) - for _ in range(4): - y = guidedFilter(np_img, y, 4, 16) - return y +# def deglaze_np_img(np_img): +# y = np_img.copy() +# for _ in range(64): +# y = cv2.bilateralFilter(y, 5, 8, 8) +# for _ in range(4): +# y = guidedFilter(np_img, y, 4, 16) +# return y -class DeglazeImage: - """Remove adversarial noise from images""" +# class DeglazeImage: +# """Remove adversarial noise from images""" - @classmethod - def INPUT_TYPES(cls): - return {"required": {"image": ("IMAGE",)}} +# @classmethod +# def INPUT_TYPES(cls): +# return {"required": {"image": ("IMAGE",)}} - CATEGORY = "mtb/image processing" +# CATEGORY = "mtb/image processing" - RETURN_TYPES = ("IMAGE",) - FUNCTION = "deglaze_image" +# RETURN_TYPES = ("IMAGE",) +# FUNCTION = "deglaze_image" - def deglaze_image(self, image): - return (np2tensor(deglaze_np_img(tensor2np(image))),) +# def deglaze_image(self, image): +# return (np2tensor(deglaze_np_img(tensor2np(image))),) class MaskToImage: @@ -489,20 +402,32 @@ class ImagePremultiply: def premultiply(self, image, mask, invert): invert = invert == "True" - image = tensor2pil(image) - mask = tensor2pil(mask).convert("L") + images = tensor2pil(image) if invert: - mask = ImageChops.invert(mask) + masks = tensor2pil(mask) # .convert("L") + else: + masks = tensor2pil(1.0 - mask) - image.putalpha(mask) + single = False + if len(mask) == 1: + single = True + + masks = [x.convert("L") for x in masks] + + out = [] + for i, img in enumerate(images): + cur_mask = masks[0] if single else masks[i] + + img.putalpha(cur_mask) + out.append(img) # if invert: # image = Image.composite(image,Image.new("RGBA", image.size, color=(0,0,0,0)), mask) # else: # image = Image.composite(Image.new("RGBA", image.size, color=(0,0,0,0)), image, mask) - return (pil2tensor(image),) + return (pil2tensor(out),) class ImageResizeFactor: @@ -733,12 +658,9 @@ class SaveImageGrid: __nodes__ = [ ColorCorrect, - HsvToRgb, - RgbToHsv, ImageCompare, - Denoise, Blur, - DeglazeImage, + # DeglazeImage, MaskToImage, ColoredImage, ImagePremultiply, diff --git a/nodes/mask.py b/nodes/mask.py index f7f76ab..6745dfe 100644 --- a/nodes/mask.py +++ b/nodes/mask.py @@ -1,6 +1,7 @@ from rembg import remove from ..utils import pil2tensor, tensor2pil from PIL import Image +import comfy.utils class ImageRemoveBackgroundRembg: @@ -65,27 +66,41 @@ class ImageRemoveBackgroundRembg: post_process_mask, bgcolor, ): - image = remove( - data=tensor2pil(image), - alpha_matting=alpha_matting == "True", - alpha_matting_foreground_threshold=alpha_matting_foreground_threshold, - alpha_matting_background_threshold=alpha_matting_background_threshold, - alpha_matting_erode_size=alpha_matting_erode_size, - session=None, - only_mask=False, - post_process_mask=post_process_mask == "True", - bgcolor=None, - ) + pbar = comfy.utils.ProgressBar(image.size(0)) + images = tensor2pil(image) - # extract the alpha to a new image - mask = image.getchannel(3) + out_img = [] + out_mask = [] + out_img_on_bg = [] - # add our bgcolor behind the image - image_on_bg = Image.new("RGBA", image.size, bgcolor) + for img in images: + img_rm = remove( + data=img, + alpha_matting=alpha_matting == "True", + alpha_matting_foreground_threshold=alpha_matting_foreground_threshold, + alpha_matting_background_threshold=alpha_matting_background_threshold, + alpha_matting_erode_size=alpha_matting_erode_size, + session=None, + only_mask=False, + post_process_mask=post_process_mask == "True", + bgcolor=None, + ) - image_on_bg.paste(image, mask=mask) + # extract the alpha to a new image + mask = img_rm.getchannel(3) - return (pil2tensor(image), pil2tensor(mask), pil2tensor(image_on_bg)) + # add our bgcolor behind the image + image_on_bg = Image.new("RGBA", img_rm.size, bgcolor) + + image_on_bg.paste(img_rm, mask=mask) + + out_img.append(img_rm) + out_mask.append(mask) + out_img_on_bg.append(image_on_bg) + + pbar.update(1) + + return (pil2tensor(out_img), pil2tensor(out_mask), pil2tensor(out_img_on_bg)) __nodes__ = [ diff --git a/nodes/number.py b/nodes/number.py index 5113a5a..529c15d 100644 --- a/nodes/number.py +++ b/nodes/number.py @@ -16,7 +16,7 @@ class IntToBool: RETURN_TYPES = ("BOOL",) FUNCTION = "int_to_bool" - CATEGORY = "number" + CATEGORY = "mtb/number" def int_to_bool(self, int): return (bool(int),) @@ -47,7 +47,7 @@ class IntToNumber: RETURN_TYPES = ("NUMBER",) FUNCTION = "int_to_number" - CATEGORY = "number" + CATEGORY = "mtb/number" def int_to_number(self, int): return (int,) @@ -78,7 +78,7 @@ class FloatToNumber: RETURN_TYPES = ("NUMBER",) FUNCTION = "float_to_number" - CATEGORY = "number" + CATEGORY = "mtb/number" def float_to_number(self, float): return (float,) diff --git a/nodes/video.py b/nodes/video.py index 6e9ae67..7e1505c 100644 --- a/nodes/video.py +++ b/nodes/video.py @@ -31,7 +31,7 @@ class LoadImageSequence: } } - CATEGORY = "video" + CATEGORY = "mtb/IO" FUNCTION = "load_image" RETURN_TYPES = ( "IMAGE", @@ -183,7 +183,7 @@ class SaveImageSequence: OUTPUT_NODE = True - CATEGORY = "image" + CATEGORY = "mtb/IO" def save_images( self, From 27500ca432d686774b991045b7cffc58c0b67faf Mon Sep 17 00:00:00 2001 From: melMass Date: Fri, 21 Jul 2023 23:44:47 +0200 Subject: [PATCH 29/70] =?UTF-8?q?fix:=20=E2=9C=A8=20Various=20widgets=20is?= =?UTF-8?q?sues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit major thanks to @pythongosssss, for his past work and help on this. Still needs some cleanup --- web/mtb_widgets.js | 551 +++++++++++++++++++++++++-------------------- 1 file changed, 311 insertions(+), 240 deletions(-) diff --git a/web/mtb_widgets.js b/web/mtb_widgets.js index ee62ec6..ec26571 100644 --- a/web/mtb_widgets.js +++ b/web/mtb_widgets.js @@ -15,7 +15,7 @@ const bboxWidget = (key, val) => { type: "BBOX", // options: val, y: 0, - value: val || [0, 0, 0, 0], + value: val?.default || [0, 0, 0, 0], options: {}, draw: function (ctx, @@ -23,59 +23,63 @@ const bboxWidget = (key, val) => { widget_width, widgetY, height) { - const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5 + const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5; - const show_text = true + const show_text = true; const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; const background_color = LiteGraph.WIDGET_BGCOLOR; const text_color = LiteGraph.WIDGET_TEXT_COLOR; const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; const H = LiteGraph.NODE_WIDGET_HEIGHT; - var margin = 15; + var numWidgets = 4; // Number of stacked widgets + if (hide) return; - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if (show_text) - ctx.roundRect(margin, widgetY, widget_width - margin * 2, H, [H * 0.5]); - else - ctx.rect(margin, widgetY, widget_width - margin * 2, H); - ctx.fill(); - if (show_text) { - if (!this.disabled) - ctx.stroke(); - ctx.fillStyle = text_color; - if (!this.disabled) { - ctx.beginPath(); - ctx.moveTo(margin + 16, widgetY + 5); - ctx.lineTo(margin + 6, widgetY + H * 0.5); - ctx.lineTo(margin + 16, widgetY + H - 5); - ctx.fill(); - ctx.beginPath(); - ctx.moveTo(widget_width - margin - 16, widgetY + 5); - ctx.lineTo(widget_width - margin - 6, widgetY + H * 0.5); - ctx.lineTo(widget_width - margin - 16, widgetY + H - 5); - ctx.fill(); + for (let i = 0; i < numWidgets; i++) { + let currentY = widgetY + i * (H + margin); // Adjust Y position for each widget + + ctx.textAlign = "left"; + ctx.strokeStyle = outline_color; + ctx.fillStyle = background_color; + ctx.beginPath(); + if (show_text) + ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]); + else + ctx.rect(margin, currentY, widget_width - margin * 2, H); + ctx.fill(); + if (show_text) { + if (!this.disabled) + ctx.stroke(); + ctx.fillStyle = text_color; + if (!this.disabled) { + ctx.beginPath(); + ctx.moveTo(margin + 16, currentY + 5); + ctx.lineTo(margin + 6, currentY + H * 0.5); + ctx.lineTo(margin + 16, currentY + H - 5); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(widget_width - margin - 16, currentY + 5); + ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5); + ctx.lineTo(widget_width - margin - 16, currentY + H - 5); + ctx.fill(); + } + ctx.fillStyle = secondary_text_color; + ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7); + ctx.fillStyle = text_color; + ctx.textAlign = "right"; + + ctx.fillText( + Number(this.value).toFixed( + this.options?.precision !== undefined + ? this.options.precision + : 3 + ), + widget_width - margin * 2 - 20, + currentY + H * 0.7 + ); } - ctx.fillStyle = secondary_text_color; - ctx.fillText(this.label || this.name, margin * 2 + 5, widgetY + H * 0.7); - ctx.fillStyle = text_color; - ctx.textAlign = "right"; - - ctx.fillText( - Number(this.value).toFixed( - this.options?.precision !== undefined - ? this.options.precision - : 3 - ), - widget_width - margin * 2 - 20, - widgetY + H * 0.7 - ); - } }, mouse: function (event, pos, node) { @@ -83,98 +87,89 @@ const bboxWidget = (key, val) => { var x = pos[0] - node.pos[0]; var y = pos[1] - node.pos[1]; var width = node.size[0]; + var H = LiteGraph.NODE_WIDGET_HEIGHT; + var margin = 5; + var numWidgets = 4; // Number of stacked widgets - if (event.type == LiteGraph.pointerevents_method + "move" && this.type == "BBOX") { - if (event.deltaX) - this.value += event.deltaX * 0.1 * (this.options?.step || 1); - if (this.options.min != null && this.value < this.options.min) { - this.value = this.options.min; - } - if (this.options.max != null && this.value > this.options.max) { - this.value = this.options.max; - } - } else if (event.type == LiteGraph.pointerevents_method + "down") { - var values = this.options?.values; - if (values && values.constructor === Function) { - values = this.options.values(w, node); - } - var values_list = null; + for (let i = 0; i < numWidgets; i++) { + let currentY = y + i * (H + margin); // Adjust Y position for each widget - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (this.type == "BBOX") { - this.value += delta * 0.1 * (this.options.step || 1); + + if (event.type == LiteGraph.pointerevents_method + "move" && this.type == "BBOX") { + if (event.deltaX) + this.value += event.deltaX * 0.1 * (this.options?.step || 1); if (this.options.min != null && this.value < this.options.min) { this.value = this.options.min; } if (this.options.max != null && this.value > this.options.max) { this.value = this.options.max; } - } else if (delta) { //clicked in arrow, used for combos - var index = -1; - this.last_mouseclick = 0; //avoids dobl click event - if (values.constructor === Object) - index = values_list.indexOf(String(this.value)) + delta; - else - index = values_list.indexOf(this.value) + delta; - if (index >= values_list.length) { - index = values_list.length - 1; + } else if (event.type == LiteGraph.pointerevents_method + "down") { + var values = this.options?.values; + if (values && values.constructor === Function) { + values = this.options.values(w, node); } - if (index < 0) { - index = 0; - } - if (values.constructor === Array) - this.value = values[index]; - else - this.value = index; - } else { //combo clicked - var text_values = values != values_list ? Object.values(values) : values; - var menu = new LiteGraph.ContextMenu(text_values, { - scale: Math.max(1, this.ds.scale), - event: event, - className: "dark", - callback: inner_clicked.bind(w) - }, - ref_window); - function inner_clicked(v, option, event) { - if (values != values_list) - v = text_values.indexOf(v); - this.value = v; - shared.inner_value_change(this, v, event); - app.canvas.setDirty(true) - return false; - } - } - } //end mousedown - else if (event.type == LiteGraph.pointerevents_method + "up" && this.type == "BBOX") { - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (event.click_time < 200 && delta == 0) { - this.prompt("Value", this.value, function (v) { - // check if v is a valid equation or a number - if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { - try {//solve the equation if possible - v = eval(v); - } catch (e) { } + var values_list = null; + + var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; + if (this.type == "BBOX") { + this.value += delta * 0.1 * (this.options.step || 1); + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min; } - this.value = Number(v); - shared.inner_value_change(this, this.value, event); - }.bind(w), - event); + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max; + } + } else if (delta) { //clicked in arrow, used for combos + var index = -1; + this.last_mouseclick = 0; //avoids dobl click event + if (values.constructor === Object) + index = values_list.indexOf(String(this.value)) + delta; + else + index = values_list.indexOf(this.value) + delta; + if (index >= values_list.length) { + index = values_list.length - 1; + } + if (index < 0) { + index = 0; + } + if (values.constructor === Array) + this.value = values[index]; + else + this.value = index; + } + } //end mousedown + else if (event.type == LiteGraph.pointerevents_method + "up" && this.type == "BBOX") { + var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; + if (event.click_time < 200 && delta == 0) { + this.prompt("Value", this.value, function (v) { + // check if v is a valid equation or a number + if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { + try {//solve the equation if possible + v = eval(v); + } catch (e) { } + } + this.value = Number(v); + shared.inner_value_change(this, this.value, event); + }.bind(w), + event); + } } + + if (old_value != this.value) + setTimeout( + function () { + shared.inner_value_change(this, this.value, event); + }.bind(this), + 20 + ); + + app.canvas.setDirty(true); } - if (old_value != this.value) - setTimeout( - function () { - shared.inner_value_change(this, this.value, event); - }.bind(this), - 20 - ); - - app.canvas.setDirty(true) - }, computeSize: function (width) { - return [width, 32]; + return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4]; }, // onDrawBackground: function (ctx) { // if (!this.flags.collapsed) return; @@ -264,7 +259,7 @@ const bboxWidgetDOM = (key, val) => { document.body.appendChild(widget.inputEl); - console.log(widget.inputEl) + console.log("Bounding Box Widget DOM", widget.inputEl) return widget } @@ -281,32 +276,49 @@ const boolWidget = (key, val, compute = false) => { value: val || false, draw: function (ctx, node, - widgetWidth, + widget_width, widgetY, height) { - const hide = this.type !== "BOOL" + const hide = this.type !== "BOOL" && app.canvas.ds.scale > 0.5 if (hide) { return } - const border = 6; - const w = 20; - ctx.fillStyle = "black"; + const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; + const background_color = LiteGraph.WIDGET_BGCOLOR; + const text_color = LiteGraph.WIDGET_TEXT_COLOR; + const H = LiteGraph.NODE_WIDGET_HEIGHT; + const arrowSize = 8; - ctx.fillRect(24, widgetY, w, w); + var margin = 15; + if (hide) return; + + var currentY = widgetY; + + ctx.textAlign = "left"; + ctx.strokeStyle = outline_color; + ctx.fillStyle = background_color; + ctx.beginPath(); + // ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]); + ctx.rect(margin, currentY, H, H); // Draw checkbox square + + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = text_color; + // ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7); + ctx.fillText(this.label || this.name, H + margin * 2, currentY + H * 0.7); + + + // Draw arrow if the value is true + // Draw checkmark if the value is true if (this.value) { - ctx.fillStyle = "white"; - ctx.fillRect(24 + border, widgetY + border, w - border * 2, height - border * 2); + ctx.fillStyle = text_color; + ctx.beginPath(); + ctx.moveTo(margin + H * 0.15, currentY + H * 0.5); + ctx.lineTo(margin + H * 0.4, currentY + H * 0.8); + ctx.lineTo(margin + H * 0.85, currentY + H * 0.2); + ctx.stroke(); } - console.log(typeof (this.value)) - // write the input name - // choose the fill based on the luminoisty of this.value color - - ctx.fillStyle = "white"; - - - ctx.font = "14px Arial"; - ctx.textAlign = "center"; - ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14); }, get value() { @@ -319,8 +331,21 @@ const boolWidget = (key, val, compute = false) => { computeSize: function (width) { return [width, 32]; }, - mouse: function (e, pos, node) { - if (e.type === "pointerdown") { + mouse: function (event, pos, node) { + // var x = pos[0] - node.pos[0]; + // var y = pos[1] - node.pos[1]; + // var width = node.size[0]; + // var H = LiteGraph.NODE_WIDGET_HEIGHT; + // var margin = 15; + + // if (event.type == LiteGraph.pointerevents_method + "down") { + // if (x > margin && x < widget_width - margin && y > widgetY && y < widgetY + H) { + // this.value = !this.value; // Toggle checkbox value + // shared.inner_value_change(this, this.value, event); + // app.canvas.setDirty(true); + // } + // } + if (event.type === "pointerdown") { // get widgets of type type : "COLOR" const widgets = node.widgets.filter(w => w.type === "BOOL"); @@ -328,7 +353,6 @@ const boolWidget = (key, val, compute = false) => { // color picker const rect = [w.last_y, w.last_y + 32]; if (pos[1] > rect[0] && pos[1] < rect[1]) { - console.log("bool", this.value) // picker.style.position = "absolute"; // picker.style.left = ( pos[0]) + "px"; // picker.style.top = ( pos[1]) + "px"; @@ -339,6 +363,7 @@ const boolWidget = (key, val, compute = false) => { // picker.style.top = (window.innerHeight / 2) + "px"; // picker.style.transform = "translate(-50%, -50%)"; // picker.style.zIndex = 1000; + console.log("Clicked a BOOL", this.value) this.value = this.value ? "false" : "true" @@ -513,9 +538,132 @@ const colorWidget = (key, val, compute = false) => { const mtb_widgets = { name: "mtb.widgets", - init: () => { }, + init: async () => { + console.log("Registering mtb.widgets") + try { + + const res = await api.fetchApi('/mtb/debug') + const msg = await res.json() + window.MTB_DEBUG = msg.enabled; + } + catch (e) { + console.error('Error:', error); + } + class MyMapNode { + constructor() { + + this.addInput("input", "number"); + this.addOutput("output", "number"); + this.addWidget("combo", "output_mode", "FLOAT", this.onChangeMode.bind(this), { + values: ["FLOAT", "INT"], + }); + + this.properties = { + src_min: 0, + src_max: 1, + target_min: 0, + target_max: 1, + output_mode: "FLOAT", + }; + + this.size = [140, 100]; + } + + static title = "Map Range"; + + onChangeMode(value) { + this.properties.output_mode = value; + } + + onExecute() { + const input = this.getInputData(0); + if (input === undefined) return; + + const src_min = this.properties.src_min; + const src_max = this.properties.src_max; + const target_min = this.properties.target_min; + const target_max = this.properties.target_max; + + let output; + + if (this.properties.output_mode === "FLOAT") { + output = ((input - src_min) / (src_max - src_min)) * (target_max - target_min) + target_min; + } else { + output = Math.floor(((input - src_min) / (src_max - src_min)) * (target_max - target_min) + target_min); + } + + this.setOutputData(0, output); + } + } + + LiteGraph.registerNodeType("mtb/math/fit", MyMapNode); + }, + + setup: () => { + app.ui.settings.addSetting({ + id: "mtb.Debug.enabled", + name: "[mtb] Enable Debug (py and js)", + type: "boolean", + defaultValue: false, + + tooltip: + "This will enable debug messages in the console and in the python console respectively", + attrs: { + style: { + fontFamily: "monospace", + }, + }, + async onChange(value) { + if (value) { + console.log("Enabled DEBUG mode") + } + window.MTB_DEBUG = value; + await api.fetchApi('/mtb/debug', { + method: 'POST', + body: JSON.stringify({ + enabled: value + + }) + }).then(response => { }).catch(error => { + console.error('Error:', error); + }); + + }, + }); + }, + getCustomWidgets: function () { + return { + BOOL: (node, inputName, inputData, app) => { + console.debug("Registering bool") + + return { + widget: node.addCustomWidget(boolWidget(inputName, inputData[1]?.default || false)), + minWidth: 150, + minHeight: 30, + }; + }, + + COLOR: (node, inputName, inputData, app) => { + console.debug("Registering color") + return { + widget: node.addCustomWidget(colorWidget(inputName, inputData[1]?.default || "#ff0000")), + minWidth: 150, + minHeight: 30, + } + }, + BBOX: (node, inputName, inputData, app) => { + console.debug("Registering bbox") + return { + widget: node.addCustomWidget(bboxWidget(inputName, inputData[1]?.default || [0, 0, 0, 0])), + minWidth: 150, + minHeight: 30, + } + + } + } + }, /** * @param {import("./types/comfy").NodeType} nodeType * @param {import("./types/comfy").NodeDef} nodeData @@ -546,12 +694,13 @@ const mtb_widgets = { for (const [key, input] of Object.entries(rinputs)) { switch (input[0]) { case "COLOR": - const colW = colorWidget(key, input[1]) - this.addCustomWidget(colW) - const associated_input = this.inputs.findIndex((i) => i.widget?.name === key); - if (associated_input !== -1) { - this.inputs[associated_input].widget = colW - } + console.log(this) + // const colW = colorWidget(key, input[1]) + // this.addCustomWidget(colW) + // const associated_input = this.inputs.findIndex((i) => i.widget?.name === key); + // if (associated_input !== -1) { + // this.inputs[associated_input].widget = colW + // } @@ -559,29 +708,16 @@ const mtb_widgets = { case "BOOL": // const widg = boolWidget(key, input[1]) // this.addCustomWidget(widg) - this.addWidget("toggle", key, false, function (value, widget, node) { - console.log(value) + // this.addWidget("toggle", key, false, function (value, widget, node) { + // console.log(value) - }) + // }) //this.removeInput(this.inputs.findIndex((i) => i.widget?.name === key)); break; case "BBOX": - - // const x_widget = ComfyWidgets.FLOAT(this, `${key}_x`, [undefined, { default: 1, min: 1, max: Number.MAX_SAFE_INTEGER }]).widget; - // const y_widget = ComfyWidgets.FLOAT(this, `${key}_y`, [undefined, { default: 1, min: 1, max: Number.MAX_SAFE_INTEGER }]).widget; - // const width_widget = ComfyWidgets.FLOAT(this, `${key}_width`, [undefined, { default: 1, min: 1, max: Number.MAX_SAFE_INTEGER }]).widget; - // const height_widget = ComfyWidgets.FLOAT(this, `${key}_height`, [undefined, { default: 1, min: 1, max: Number.MAX_SAFE_INTEGER }]).widget; - - - - // this.addWidget("number", `${key}_x`, false, function (value, widget, node) { }) - // this.addWidget("number", `${key}_y`, false, function (value, widget, node) { }) - // this.addWidget("number", `${key}_width`, false, function (value, widget, node) { }) - // this.addWidget("number", `${key}_height`, false, function (value, widget, node) { }) - - const bboxW = bboxWidget(key, input[1]) - this.addCustomWidget(bboxW) + // const bboxW = bboxWidget(key, input[1]) + // this.addCustomWidget(bboxW) break; default: break @@ -603,40 +739,6 @@ const mtb_widgets = { }; } - //- On initial configure of nodes hide all converted widgets - const origOnConfigure = nodeType.prototype.onConfigure; - nodeType.prototype.onConfigure = function () { - const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined; - console.log(this) - - if (this.inputs) { - for (const input of this.inputs) { - console.log(input) - if (input.widget) { - // if (newTypes.includes(input.type)) { - - console.log(input.widget) - if (input.widget.hidden) { - console.debug(`Already hidden skipping ${input.widget.name}`) - continue - } - const w = this.widgets.find((w) => w.name === input.widget.name); - if (w) { - console.log(`hidding ${w.name} from ${this.label}`) - shared.hideWidget(this, w); - } else { - console.log(`converting to widget ${w}`) - - shared.convertToWidget(this, input) - } - } - } - } - - return r; - }; - - //- Extra menus const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function (_, options) { @@ -646,11 +748,12 @@ const mtb_widgets = { let toWidget = []; for (const w of this.widgets) { if (w.type === shared.CONVERTED_TYPE) { - toWidget.push({ - content: `Convert ${w.name} to widget`, - callback: () => shared.convertToWidget(this, w), - }); - } else { + //- This is already handled by widgetinputs.js + // toWidget.push({ + // content: `Convert ${w.name} to widget`, + // callback: () => shared.convertToWidget(this, w), + // }); + } else if (newTypes.includes(w.type)) { const config = nodeData?.input?.required[w.name] || nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}]; toInput.push({ @@ -673,22 +776,6 @@ const mtb_widgets = { } - - - // if (nodeData.input && nodeData.input.includes("BOOL")) { - - // addMenuHandler(nodeType, function (_, options) { - // options.unshift({ - // content: "convert bool input to widget", - // callback: () => { - - // console.log("Nahh not yet") - // }, - // }); - // }); - - // } - //- Extending Python Nodes switch (nodeData.name) { case "Psd Save (mtb)": { @@ -725,7 +812,7 @@ const mtb_widgets = { raw_iteration._value = 0 // Object.defineProperty(raw_iteration, "value", { - // get() { + // get() { // return this._value // }, // set(value) { @@ -800,28 +887,12 @@ frame: ${this.value % total_frames.value}`; loop_preview.value = `current loop: ${raw_loop.value + 1}/${loop_count.value}` } - // ComfyWidgets.hideWidget() - // if (raw_iteration > 0) { - // raw_iteration.inputEl.readOnly = true - // raw_iteration.inputEl.style.opacity = 0.6; - // console.log(raw_iteration) - // raw_iteration.onRemove?.(); - // console.log(this.widgets[raw_iteration].type); - - // raw_iteration.afterQueued = function () { - // this.value++ - // } - // } - - - } - //LiteGraph.ALWAYS const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (data) { - console.log("executed") + console.log("executed node", this) onExecuted?.apply(this, data) if (this.widgets) { const pos = this.widgets.findIndex((w) => w.name === "preview"); From 44eaae5c79f4dbec344053d945e7275be5c3c0a5 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 00:12:13 +0200 Subject: [PATCH 30/70] =?UTF-8?q?feat:=20=F0=9F=9A=A7=20add=20my=20CLIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit just convenience tools, not exposed but usable from CI etc... still need some work. --- interpolate_frames.py | 142 +++++++++++++++++++++++++++ scripts/a111_extract.py | 112 +++++++++++++++++++++ scripts/comfy_meta.py | 213 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 467 insertions(+) create mode 100644 interpolate_frames.py create mode 100644 scripts/a111_extract.py create mode 100644 scripts/comfy_meta.py diff --git a/interpolate_frames.py b/interpolate_frames.py new file mode 100644 index 0000000..6c235f0 --- /dev/null +++ b/interpolate_frames.py @@ -0,0 +1,142 @@ +import glob +from pathlib import Path +import uuid +import sys +from typing import List + +sys.path.append((Path(__file__).parent / "extern").as_posix()) + + +import argparse +from rich_argparse import RichHelpFormatter +from rich.console import Console +from rich.progress import Progress + +import numpy as np +import subprocess + + +def write_prores_444_video(output_file, frames: List[np.ndarray], fps): + # Convert float images to the range of 0-65535 (12-bit color depth) + frames = [(frame * 65535).clip(0, 65535).astype(np.uint16) for frame in frames] + + height, width, _ = frames[0].shape + + # Prepare the FFmpeg command + command = [ + "ffmpeg", + "-y", # Overwrite output file if it already exists + "-f", + "rawvideo", + "-vcodec", + "rawvideo", + "-s", + f"{width}x{height}", + "-pix_fmt", + "rgb48le", + "-r", + str(fps), + "-i", + "-", + "-c:v", + "prores_ks", + "-profile:v", + "4", + "-pix_fmt", + "yuva444p10le", + "-r", + str(fps), + "-y", # Overwrite output file if it already exists + output_file, + ] + + process = subprocess.Popen(command, stdin=subprocess.PIPE) + + for frame in frames: + process.stdin.write(frame.tobytes()) + + process.stdin.close() + process.wait() + + +if __name__ == "__main__": + default_output = f"./output_{uuid.uuid4()}.mov" + parser = argparse.ArgumentParser( + description="FILM frame interpolation", formatter_class=RichHelpFormatter + ) + parser.add_argument("inputs", nargs="*", help="Input image files") + parser.add_argument("--output", help="Output JSON file", default=default_output) + parser.add_argument("-v", "--verbose", action="store_true", help="Verbose mode") + parser.add_argument( + "--glob", help="Enable glob pattern matching", metavar="PATTERN" + ) + parser.add_argument( + "--interpolate", type=int, default=4, help="Time for interpolated frames" + ) + parser.add_argument("--fps", type=int, default=30, help="Out FPS") + align = 64 + block_width = 2 + block_height = 2 + + args = parser.parse_args() + + # - checks + if not args.glob and not args.inputs: + parser.error("Either --glob flag or inputs must be provided.") + if args.glob: + glob_pattern = args.glob + try: + pattern_path = str(Path(glob_pattern).expanduser().resolve()) + + if not any(glob.glob(pattern_path)): + raise ValueError(f"No files found for glob pattern: {glob_pattern}") + except Exception as e: + console = Console() + console.print( + f"[bold red]Error: Invalid glob pattern '{glob_pattern}': {e}[/bold red]" + ) + + exit(1) + else: + glob_pattern = None + + input_files: List[Path] = [] + + if glob_pattern: + input_files = [ + Path(p) + for p in list(glob.glob(str(Path(glob_pattern).expanduser().resolve()))) + ] + else: + input_files = [Path(p) for p in args.inputs] + + console = Console() + console.print("Input Files:", style="bold", end=" ") + console.print(f"{len(input_files):03d} files", style="cyan") + # for input_file in args.inputs: + # console.print(f"- {input_file}", style="cyan") + console.print("\nOutput File:", style="bold", end=" ") + console.print(f"{Path(args.output).resolve().absolute()}", style="cyan") + + with Progress(console=console, auto_refresh=True) as progress: + from frame_interpolation.eval import util + from frame_interpolation.eval import util, interpolator + + # files = Path(pth).rglob("*.png") + + model = interpolator.Interpolator( + "G:/MODELS/FILM/pretrained_models/film_net/Style", None + ) # [2,2] + + task = progress.add_task("[cyan]Interpolating frames...", total=1) + + frames = list( + util.interpolate_recursively_from_files( + [x.as_posix() for x in input_files], args.interpolate, model + ) + ) + + # mediapy.write_video(args.output, frames, fps=args.fps) + write_prores_444_video(args.output, frames, fps=args.fps) + progress.update(task, advance=1) + progress.refresh() diff --git a/scripts/a111_extract.py b/scripts/a111_extract.py new file mode 100644 index 0000000..7e38b0b --- /dev/null +++ b/scripts/a111_extract.py @@ -0,0 +1,112 @@ +from pathlib import Path +from PIL import Image +from PIL.PngImagePlugin import PngImageFile, PngInfo +import json +from pprint import pprint +import argparse +from rich.console import Console +from rich.progress import Progress +from rich_argparse import RichHelpFormatter + + +def parse_a111(params, verbose=False): + # params = [p.split(": ") for p in params.split("\n")] + params = params.split("\n") + + prompt = params[0].strip() + neg = params[1].split(":")[1].strip() + + settings = {} + try: + settings = { + s.split(":")[0].strip(): s.split(":")[1].strip() + for s in params[2].split(",") + } + + except IndexError: + settings = {"raw": params[2].strip()} + + if verbose: + print(f"PROMPT: {prompt}") + print(f"NEG: {neg}") + print("SETTINGS:") + pprint(settings, indent=4) + + return {"prompt": prompt, "negative": neg, "settings": settings} + + +import glob + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Crude metadata extractor from A111 pngs", + formatter_class=RichHelpFormatter + ) + parser.add_argument("inputs", nargs="*", help="Input image files") + parser.add_argument("--output", help="Output JSON file") + parser.add_argument("-v", "--verbose", action="store_true", help="Verbose mode") + parser.add_argument( + "--glob", help="Enable glob pattern matching", metavar="PATTERN" + ) + + args = parser.parse_args() + + # - checks + if not args.glob and not args.inputs: + parser.error("Either --glob flag or inputs must be provided.") + if args.glob: + glob_pattern = args.glob + try: + pattern_path = str(Path(glob_pattern).expanduser().resolve()) + + if not any(glob.glob(pattern_path)): + raise ValueError(f"No files found for glob pattern: {glob_pattern}") + except Exception as e: + console = Console() + console.print( + f"[bold red]Error: Invalid glob pattern '{glob_pattern}': {e}[/bold red]" + ) + + exit(1) + else: + glob_pattern = None + + input_files = [] + + if glob_pattern: + input_files = list(glob.glob(str(Path(glob_pattern).expanduser().resolve()))) + else: + input_files = [Path(p) for p in args.inputs] + + console = Console() + console.print("Input Files:", style="bold", end=" ") + console.print(f"{len(input_files):03d} files", style="cyan") + # for input_file in args.inputs: + # console.print(f"- {input_file}", style="cyan") + console.print("\nOutput File:", style="bold", end=" ") + console.print(f"{Path(args.output).resolve().absolute()}", style="cyan") + + with Progress(console=console, auto_refresh=True) as progress: + # files = Path(pth).rglob("*.png") + unique_info = {} + last = None + + task = progress.add_task("[cyan]Extracting meta...", total=len(input_files) + 1) + for p in input_files: + im = Image.open(p) + parsed = parse_a111(im.info["parameters"], args.verbose) + + if parsed != last: + unique_info[Path(p).stem] = parsed + + last = parsed + progress.update(task, advance=1) + progress.refresh() + + unique_info = json.dumps(unique_info, indent=4) + with open(args.output, "w") as f: + f.write(unique_info) + progress.update(task, advance=1) + progress.refresh() + + console.print("\nProcessing completed!", style="bold green") diff --git a/scripts/comfy_meta.py b/scripts/comfy_meta.py new file mode 100644 index 0000000..cb9cb1e --- /dev/null +++ b/scripts/comfy_meta.py @@ -0,0 +1,213 @@ +import argparse +import json +from PIL import Image, PngImagePlugin +from rich.console import Console +from rich import print +from rich_argparse import RichHelpFormatter +import os +from pathlib import Path + +console = Console() + +# BNK_CutoffSetRegions +# BNK_CutoffRegionsToConditioning +# BNK_CutoffBasePrompt + + +# Extracts metadata from a PNG image and returns it as a dictionary +def extract_metadata(image_path): + image = Image.open(image_path) + prompt = image.info.get("prompt", "") + workflow = image.info.get("workflow", "") + + if workflow: + workflow = json.loads(workflow) + + if prompt: + prompt = json.loads(prompt) + + console.print(f"Metadata extracted from [cyan]{image_path}[/cyan].") + + return { + "prompt": prompt, + "workflow": workflow, + } + + +# Embeds metadata into a PNG image +def embed_metadata(image_path, metadata): + image = Image.open(image_path) + o_metadata = image.info + + pnginfo = PngImagePlugin.PngInfo() + if prompt := metadata.get("prompt"): + pnginfo.add_text("prompt", json.dumps(prompt)) + elif "prompt" in o_metadata: + pnginfo.add_text("prompt", o_metadata["prompt"]) + + if workflow := metadata.get("workflow"): + pnginfo.add_text("workflow", json.dumps(workflow)) + elif "workflow" in o_metadata: + pnginfo.add_text("workflow", o_metadata["workflow"]) + + imgp = Path(image_path) + output = imgp.with_stem(f"{imgp.stem}_comfy_embed") + index = 1 + while output.exists(): + output = imgp.with_stem(f"{imgp.stem}_{index}_comfy_embed").with_suffix(".png") + index += 1 + + image.save(output, pnginfo=pnginfo) + console.print(f"Metadata embedded into [cyan]{output}[/cyan].") + + +# CLI subcommand: extract +def extract(args): + input_files = [] + for input_path in args.input: + if os.path.isdir(input_path): + folder_path = input_path + input_files.extend( + [ + os.path.join(folder_path, file_name) + for file_name in os.listdir(folder_path) + if file_name.lower().endswith((".png", ".jpg", ".jpeg")) + ] + ) + else: + input_files.append(input_path) + + if len(input_files) == 1: + metadata = extract_metadata(input_files[0]) + if args.print_output: + print(json.dumps(metadata, indent=4)) + else: + if not args.output: + output = Path(input_files[0]).with_suffix(".json") + index = 1 + while output.exists(): + output = ( + Path(input_files[0]) + .with_stem(f"{Path(input_files[0]).stem}_{index}") + .with_suffix(".json") + ) + index += 1 + else: + output = args.output + with open(output, "w") as file: + json.dump(metadata, file, indent=4) + console.print(f"Metadata extracted and saved to [cyan]{output}[/cyan].") + else: + metadata_dict = {} + for input_file in input_files: + metadata = extract_metadata(input_file) + filename = os.path.basename(input_file) + output = ( + Path(args.output) / f"{filename}.json" + if args.output + else Path(input_file).with_suffix(".json") + ) + index = 1 + while output.exists(): + output = Path(args.output).parent / f"{filename}_{index}.json" + index += 1 + with open(output, "w") as file: + json.dump(metadata, file, indent=4) + metadata_dict[filename] = metadata + if args.output: + with open(args.output, "w") as file: + json.dump(metadata_dict, file, indent=4) + console.print( + f"Metadata extracted and saved to [cyan]{args.output}[/cyan]." + ) + else: + console.print("Multiple metadata files created.") + + +# CLI subcommand: embed +def embed(args): + input_files = [] + for input_path in args.input: + if os.path.isdir(input_path): + folder_path = input_path + input_files.extend( + [ + os.path.join(folder_path, file_name) + for file_name in os.listdir(folder_path) + if file_name.lower().endswith(".json") + ] + ) + else: + input_files.append(input_path) + + for input_file in input_files: + with open(input_file) as file: + metadata = json.load(file) + image_path = input_file.replace(".json", ".png") + if args.output: + output_dir = args.output + if os.path.isdir(output_dir): + output_path = os.path.join(output_dir, os.path.basename(image_path)) + index = 1 + while os.path.exists(output_path): + output_path = os.path.join( + output_dir, + f"{os.path.basename(image_path)}_{index}.png", + ) + index += 1 + else: + output_path = output_dir + else: + output_path = image_path.replace(".png", "_comfy_embed.png") + + embed_metadata(image_path, metadata) + # os.rename(image_path, output_path) + console.print(f"Metadata embedded into [cyan]{output_path}[/cyan].") + + +if __name__ == "__main__": + # Create the main CLI parser + parser = argparse.ArgumentParser( + prog="image-metadata-cli", formatter_class=RichHelpFormatter + ) + subparsers = parser.add_subparsers(title="subcommands") + + # Parser for the "extract" subcommand + extract_parser = subparsers.add_parser( + "extract", + help="Extract metadata from PNG image(s) or folder", + formatter_class=RichHelpFormatter, + ) + extract_parser.add_argument( + "input", nargs="+", help="Input PNG image file(s) or folder path" + ) + extract_parser.add_argument( + "--print", + dest="print_output", + action="store_true", + help="Print the output to stdout", + ) + extract_parser.add_argument("--output", help="Output JSON file(s) or directory") + extract_parser.set_defaults(func=extract) + + # Parser for the "embed" subcommand + embed_parser = subparsers.add_parser( + "embed", + help="Embed metadata into PNG image(s) or folder", + formatter_class=RichHelpFormatter, + ) + embed_parser.add_argument( + "input", nargs="+", help="Input JSON file(s) or folder path" + ) + embed_parser.add_argument("--output", help="Output PNG image file(s) or directory") + embed_parser.set_defaults(func=embed) + + # Parse the command-line arguments and execute the appropriate subcommand + args = parser.parse_args() + if hasattr(args, "func"): + try: + args.func(args) + except ValueError as e: + console.print(f"[bold red]Error:[/bold red] {str(e)}") + else: + parser.print_help() From 68c250e890dacae9f627b0d266ad6dcab0fa0c8b Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 00:21:28 +0200 Subject: [PATCH 31/70] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20remove?= =?UTF-8?q?=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/mtb_widgets.js | 48 ---------------------------------------------- 1 file changed, 48 deletions(-) diff --git a/web/mtb_widgets.js b/web/mtb_widgets.js index ec26571..8a3db72 100644 --- a/web/mtb_widgets.js +++ b/web/mtb_widgets.js @@ -549,54 +549,6 @@ const mtb_widgets = { catch (e) { console.error('Error:', error); } - class MyMapNode { - constructor() { - - this.addInput("input", "number"); - this.addOutput("output", "number"); - this.addWidget("combo", "output_mode", "FLOAT", this.onChangeMode.bind(this), { - values: ["FLOAT", "INT"], - }); - - this.properties = { - src_min: 0, - src_max: 1, - target_min: 0, - target_max: 1, - output_mode: "FLOAT", - }; - - this.size = [140, 100]; - } - - static title = "Map Range"; - - onChangeMode(value) { - this.properties.output_mode = value; - } - - onExecute() { - const input = this.getInputData(0); - if (input === undefined) return; - - const src_min = this.properties.src_min; - const src_max = this.properties.src_max; - const target_min = this.properties.target_min; - const target_max = this.properties.target_max; - - let output; - - if (this.properties.output_mode === "FLOAT") { - output = ((input - src_min) / (src_max - src_min)) * (target_max - target_min) + target_min; - } else { - output = Math.floor(((input - src_min) / (src_max - src_min)) * (target_max - target_min) + target_min); - } - - this.setOutputData(0, output); - } - } - - LiteGraph.registerNodeType("mtb/math/fit", MyMapNode); }, setup: () => { From c2e83794faeb8da708c98908882e38b2a42827bd Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 01:17:54 +0200 Subject: [PATCH 32/70] =?UTF-8?q?fix:=20=F0=9F=8E=A8=20refactor=20and=20ad?= =?UTF-8?q?d=20Gif=20preview=20on=20node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/debug.js | 113 +----- web/mtb_widgets.js | 915 +++++++++++++++++++++++++-------------------- 2 files changed, 517 insertions(+), 511 deletions(-) diff --git a/web/debug.js b/web/debug.js index 49cfd44..00079e1 100644 --- a/web/debug.js +++ b/web/debug.js @@ -1,118 +1,11 @@ import { app } from "/scripts/app.js"; import * as shared from '/extensions/mtb/comfy_shared.js' import { log } from '/extensions/mtb/comfy_shared.js' +import { MtbWidgets } from '/extensions/mtb/mtb_widgets.js' // TODO: respect inputs order... -const DEBUG_WIDGETS = { - "image": (val, index) => { - const w = { - name: `anything_${index}`, - type: "image", - value: val, - draw: function (ctx, - node, - widgetWidth, - widgetY, - height) { - const [cw, ch] = this.computeSize(widgetWidth) - shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) - }, - computeSize: function (width) { - const ratio = this.inputRatio || 1; - if (width) { - return [width, width / ratio + 4] - } - return [128, 128] - }, - onRemove: function () { - if (this.inputEl) { - this.inputEl.remove(); - } - } - } - - w.inputEl = document.createElement("img"); - w.inputEl.src = "data:image/jpeg;base64," + w.value; - w.inputEl.onload = function () { - w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight; - } - document.body.appendChild(w.inputEl); - return w - }, - "text": (val, index) => { - const w = { - name: `anything_${index}`, - type: "debug_text", - val: val, - draw: function (ctx, - node, - widgetWidth, - widgetY, - height) { - // const [cw, ch] = this.computeSize(widgetWidth) - shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) - }, - computeSize: function (width) { - const value = this.inputEl.innerHTML - if (!value) { - return [32, 32] - } - if (!width) { - log(`No width ${this.parent.size}`) - } - - const fontSize = 25; // Assuming 1rem = 16px - - const oldFont = app.ctx.font - app.ctx.font = `${fontSize}px Arial`; - - const words = value.split(" "); - const lines = []; - let currentLine = ""; - for (const word of words) { - const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}`; - - const testWidth = app.ctx.measureText(testLine).width; - - // log(`Testing line ${testLine}, width: ${testWidth}, width: ${width}, ratio: ${testWidth / width}`) - if (testWidth > width) { - lines.push(currentLine); - currentLine = word; - } else { - currentLine = testLine; - } - } - app.ctx.font = oldFont; - lines.push(currentLine); - - // Step 3: Calculate the widget width and height - const textHeight = lines.length * (fontSize + 2); // You can adjust the line height (2 in this case) - const maxLineWidth = lines.reduce((maxWidth, line) => Math.max(maxWidth, app.ctx.measureText(line).width), 0); - const widgetWidth = Math.max(width || this.width || 32, maxLineWidth); - const widgetHeight = textHeight + 10; // Additional padding for spacing - return [widgetWidth, widgetHeight + 4] - - }, - onRemove: function () { - if (this.inputEl) { - this.inputEl.remove(); - } - - } - } - w.inputEl = document.createElement("p"); - w.inputEl.style.textAlign = "center"; - w.inputEl.style.fontSize = "1.5em"; - w.inputEl.style.color = "var(--input-text)"; - w.inputEl.style.fontFamily = "monospace"; - w.inputEl.innerHTML = val - document.body.appendChild(w.inputEl); - - return w - } -} app.registerExtension({ name: "mtb.Debug", @@ -155,14 +48,14 @@ app.registerExtension({ let widgetI = 1 if (message.text) { for (const txt of message.text) { - const w = this.addCustomWidget(DEBUG_WIDGETS["text"](txt, widgetI)) + const w = this.addCustomWidget(MtbWidgets.DEBUG_STRING(txt, widgetI)) w.parent = this; widgetI++; } } if (message.b64_images) { for (const img of message.b64_images) { - const w = this.addCustomWidget(DEBUG_WIDGETS["image"](img, widgetI)) + const w = this.addCustomWidget(MtbWidgets.DEBUG_IMG(img, widgetI)) w.parent = this; widgetI++; } diff --git a/web/mtb_widgets.js b/web/mtb_widgets.js index 8a3db72..97927b3 100644 --- a/web/mtb_widgets.js +++ b/web/mtb_widgets.js @@ -8,188 +8,512 @@ import { ComfyWidgets } from "/scripts/widgets.js"; const newTypes = ["BOOL", "COLOR", "BBOX"] -const bboxWidget = (key, val) => { - /** @type {import("./types/litegraph").IWidget} */ - const widget = { - name: key, - type: "BBOX", - // options: val, - y: 0, - value: val?.default || [0, 0, 0, 0], - options: {}, - draw: function (ctx, - node, - widget_width, - widgetY, - height) { - const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5; +export const MtbWidgets = { + BBOX: (key, val) => { + /** @type {import("./types/litegraph").IWidget} */ + const widget = { + name: key, + type: "BBOX", + // options: val, + y: 0, + value: val?.default || [0, 0, 0, 0], + options: {}, - const show_text = true; - const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; - const background_color = LiteGraph.WIDGET_BGCOLOR; - const text_color = LiteGraph.WIDGET_TEXT_COLOR; - const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; - const H = LiteGraph.NODE_WIDGET_HEIGHT; + draw: function (ctx, + node, + widget_width, + widgetY, + height) { + const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5; - var margin = 15; - var numWidgets = 4; // Number of stacked widgets + const show_text = true; + const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; + const background_color = LiteGraph.WIDGET_BGCOLOR; + const text_color = LiteGraph.WIDGET_TEXT_COLOR; + const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; + const H = LiteGraph.NODE_WIDGET_HEIGHT; - if (hide) return; + var margin = 15; + var numWidgets = 4; // Number of stacked widgets - for (let i = 0; i < numWidgets; i++) { - let currentY = widgetY + i * (H + margin); // Adjust Y position for each widget + if (hide) return; - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if (show_text) - ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]); - else - ctx.rect(margin, currentY, widget_width - margin * 2, H); - ctx.fill(); - if (show_text) { - if (!this.disabled) - ctx.stroke(); - ctx.fillStyle = text_color; - if (!this.disabled) { - ctx.beginPath(); - ctx.moveTo(margin + 16, currentY + 5); - ctx.lineTo(margin + 6, currentY + H * 0.5); - ctx.lineTo(margin + 16, currentY + H - 5); - ctx.fill(); - ctx.beginPath(); - ctx.moveTo(widget_width - margin - 16, currentY + 5); - ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5); - ctx.lineTo(widget_width - margin - 16, currentY + H - 5); - ctx.fill(); + for (let i = 0; i < numWidgets; i++) { + let currentY = widgetY + i * (H + margin); // Adjust Y position for each widget + + ctx.textAlign = "left"; + ctx.strokeStyle = outline_color; + ctx.fillStyle = background_color; + ctx.beginPath(); + if (show_text) + ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]); + else + ctx.rect(margin, currentY, widget_width - margin * 2, H); + ctx.fill(); + if (show_text) { + if (!this.disabled) + ctx.stroke(); + ctx.fillStyle = text_color; + if (!this.disabled) { + ctx.beginPath(); + ctx.moveTo(margin + 16, currentY + 5); + ctx.lineTo(margin + 6, currentY + H * 0.5); + ctx.lineTo(margin + 16, currentY + H - 5); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(widget_width - margin - 16, currentY + 5); + ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5); + ctx.lineTo(widget_width - margin - 16, currentY + H - 5); + ctx.fill(); + } + ctx.fillStyle = secondary_text_color; + ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7); + ctx.fillStyle = text_color; + ctx.textAlign = "right"; + + ctx.fillText( + Number(this.value).toFixed( + this.options?.precision !== undefined + ? this.options.precision + : 3 + ), + widget_width - margin * 2 - 20, + currentY + H * 0.7 + ); } - ctx.fillStyle = secondary_text_color; - ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7); - ctx.fillStyle = text_color; - ctx.textAlign = "right"; - - ctx.fillText( - Number(this.value).toFixed( - this.options?.precision !== undefined - ? this.options.precision - : 3 - ), - widget_width - margin * 2 - 20, - currentY + H * 0.7 - ); } - } - }, - mouse: function (event, pos, node) { - var old_value = this.value; - var x = pos[0] - node.pos[0]; - var y = pos[1] - node.pos[1]; - var width = node.size[0]; - var H = LiteGraph.NODE_WIDGET_HEIGHT; - var margin = 5; - var numWidgets = 4; // Number of stacked widgets + }, + mouse: function (event, pos, node) { + var old_value = this.value; + var x = pos[0] - node.pos[0]; + var y = pos[1] - node.pos[1]; + var width = node.size[0]; + var H = LiteGraph.NODE_WIDGET_HEIGHT; + var margin = 5; + var numWidgets = 4; // Number of stacked widgets - for (let i = 0; i < numWidgets; i++) { - let currentY = y + i * (H + margin); // Adjust Y position for each widget + for (let i = 0; i < numWidgets; i++) { + let currentY = y + i * (H + margin); // Adjust Y position for each widget - if (event.type == LiteGraph.pointerevents_method + "move" && this.type == "BBOX") { - if (event.deltaX) - this.value += event.deltaX * 0.1 * (this.options?.step || 1); - if (this.options.min != null && this.value < this.options.min) { - this.value = this.options.min; - } - if (this.options.max != null && this.value > this.options.max) { - this.value = this.options.max; - } - } else if (event.type == LiteGraph.pointerevents_method + "down") { - var values = this.options?.values; - if (values && values.constructor === Function) { - values = this.options.values(w, node); - } - var values_list = null; - - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (this.type == "BBOX") { - this.value += delta * 0.1 * (this.options.step || 1); + if (event.type == LiteGraph.pointerevents_method + "move" && this.type == "BBOX") { + if (event.deltaX) + this.value += event.deltaX * 0.1 * (this.options?.step || 1); if (this.options.min != null && this.value < this.options.min) { this.value = this.options.min; } if (this.options.max != null && this.value > this.options.max) { this.value = this.options.max; } - } else if (delta) { //clicked in arrow, used for combos - var index = -1; - this.last_mouseclick = 0; //avoids dobl click event - if (values.constructor === Object) - index = values_list.indexOf(String(this.value)) + delta; - else - index = values_list.indexOf(this.value) + delta; - if (index >= values_list.length) { - index = values_list.length - 1; + } else if (event.type == LiteGraph.pointerevents_method + "down") { + var values = this.options?.values; + if (values && values.constructor === Function) { + values = this.options.values(w, node); } - if (index < 0) { - index = 0; - } - if (values.constructor === Array) - this.value = values[index]; - else - this.value = index; - } - } //end mousedown - else if (event.type == LiteGraph.pointerevents_method + "up" && this.type == "BBOX") { - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (event.click_time < 200 && delta == 0) { - this.prompt("Value", this.value, function (v) { - // check if v is a valid equation or a number - if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { - try {//solve the equation if possible - v = eval(v); - } catch (e) { } + var values_list = null; + + var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; + if (this.type == "BBOX") { + this.value += delta * 0.1 * (this.options.step || 1); + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min; } - this.value = Number(v); - shared.inner_value_change(this, this.value, event); - }.bind(w), - event); + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max; + } + } else if (delta) { //clicked in arrow, used for combos + var index = -1; + this.last_mouseclick = 0; //avoids dobl click event + if (values.constructor === Object) + index = values_list.indexOf(String(this.value)) + delta; + else + index = values_list.indexOf(this.value) + delta; + if (index >= values_list.length) { + index = values_list.length - 1; + } + if (index < 0) { + index = 0; + } + if (values.constructor === Array) + this.value = values[index]; + else + this.value = index; + } + } //end mousedown + else if (event.type == LiteGraph.pointerevents_method + "up" && this.type == "BBOX") { + var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; + if (event.click_time < 200 && delta == 0) { + this.prompt("Value", this.value, function (v) { + // check if v is a valid equation or a number + if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { + try {//solve the equation if possible + v = eval(v); + } catch (e) { } + } + this.value = Number(v); + shared.inner_value_change(this, this.value, event); + }.bind(w), + event); + } } + + if (old_value != this.value) + setTimeout( + function () { + shared.inner_value_change(this, this.value, event); + }.bind(this), + 20 + ); + + app.canvas.setDirty(true); } - if (old_value != this.value) - setTimeout( - function () { - shared.inner_value_change(this, this.value, event); - }.bind(this), - 20 - ); + }, + computeSize: function (width) { + return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4]; + }, + // onDrawBackground: function (ctx) { + // if (!this.flags.collapsed) return; + // this.inputEl.style.display = "block"; + // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; + // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; + // }, + // onInputChange: function (e) { + // const property = e.target.dataset.property; + // const bbox = this.getInputData(0); + // if (!bbox) return; + // bbox[property] = parseFloat(e.target.value); + // this.setOutputData(0, bbox); + // } + } - app.canvas.setDirty(true); + widget.desc = "Represents a Bounding Box with x, y, width, and height."; + return widget + + }, + BOOL: (key, val, compute = false) => { + /** @type {import("/types/litegraph").IWidget} */ + const widget = { + name: key, + type: "BOOL", + options: { default: false }, + y: 0, + value: val || false, + draw: function (ctx, + node, + widget_width, + widgetY, + height) { + const hide = this.type !== "BOOL" && app.canvas.ds.scale > 0.5 + if (hide) { + return + } + const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; + const background_color = LiteGraph.WIDGET_BGCOLOR; + const text_color = LiteGraph.WIDGET_TEXT_COLOR; + const H = LiteGraph.NODE_WIDGET_HEIGHT; + const arrowSize = 8; + + var margin = 15; + if (hide) return; + + var currentY = widgetY; + + ctx.textAlign = "left"; + ctx.strokeStyle = outline_color; + ctx.fillStyle = background_color; + ctx.beginPath(); + // ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]); + ctx.rect(margin, currentY, H, H); // Draw checkbox square + + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = text_color; + // ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7); + ctx.fillText(this.label || this.name, H + margin * 2, currentY + H * 0.7); + + + // Draw arrow if the value is true + // Draw checkmark if the value is true + if (this.value) { + ctx.fillStyle = text_color; + ctx.beginPath(); + ctx.moveTo(margin + H * 0.15, currentY + H * 0.5); + ctx.lineTo(margin + H * 0.4, currentY + H * 0.8); + ctx.lineTo(margin + H * 0.85, currentY + H * 0.2); + ctx.stroke(); + } + + }, + get value() { + + return this.inputEl.value === "true"; + }, + set value(x) { + this.inputEl.value = x; + }, + computeSize: function (width) { + return [width, 32]; + }, + mouse: function (event, pos, node) { + // var x = pos[0] - node.pos[0]; + // var y = pos[1] - node.pos[1]; + // var width = node.size[0]; + // var H = LiteGraph.NODE_WIDGET_HEIGHT; + // var margin = 15; + + // if (event.type == LiteGraph.pointerevents_method + "down") { + // if (x > margin && x < widget_width - margin && y > widgetY && y < widgetY + H) { + // this.value = !this.value; // Toggle checkbox value + // shared.inner_value_change(this, this.value, event); + // app.canvas.setDirty(true); + // } + // } + if (event.type === "pointerdown") { + // get widgets of type type : "COLOR" + const widgets = node.widgets.filter(w => w.type === "BOOL"); + + for (const w of widgets) { + // color picker + const rect = [w.last_y, w.last_y + 32]; + if (pos[1] > rect[0] && pos[1] < rect[1]) { + // picker.style.position = "absolute"; + // picker.style.left = ( pos[0]) + "px"; + // picker.style.top = ( pos[1]) + "px"; + + // place at screen center + // picker.style.position = "absolute"; + // picker.style.left = (window.innerWidth / 2) + "px"; + // picker.style.top = (window.innerHeight / 2) + "px"; + // picker.style.transform = "translate(-50%, -50%)"; + // picker.style.zIndex = 1000; + console.log("Clicked a BOOL", this.value) + + this.value = this.value ? "false" : "true" + + } + } + } + } + } + + // create a checkbox + widget.inputEl = document.createElement("input") + widget.inputEl.type = "checkbox" + widget.inputEl.value = false + document.body.appendChild(widget.inputEl); + return widget + + }, + COLOR: (key, val, compute = false) => { + /** @type {import("/types/litegraph").IWidget} */ + const widget = {} + widget.y = 0 + widget.name = key; + widget.type = "COLOR"; + widget.options = { default: "#ff0000" }; + widget.value = val || "#ff0000"; + widget.draw = function (ctx, + node, + widgetWidth, + widgetY, + height) { + const hide = this.type !== "COLOR" && app.canvas.ds.scale > 0.5 + if (hide) { + return } - }, - computeSize: function (width) { - return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4]; - }, - // onDrawBackground: function (ctx) { - // if (!this.flags.collapsed) return; - // this.inputEl.style.display = "block"; - // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; - // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; - // }, - // onInputChange: function (e) { - // const property = e.target.dataset.property; - // const bbox = this.getInputData(0); - // if (!bbox) return; - // bbox[property] = parseFloat(e.target.value); - // this.setOutputData(0, bbox); - // } + const border = 3; + // draw a rect with a border and a fill color + ctx.fillStyle = "#000"; + ctx.fillRect(0, widgetY, widgetWidth, height); + ctx.fillStyle = this.value; + ctx.fillRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); + // write the input name + // choose the fill based on the luminoisty of this.value color + const color = parseCss(this.value.default || this.value) + if (!color) { + return + } + ctx.fillStyle = shared.isColorBright(color.values, 125) ? "#000" : "#fff"; + + + ctx.font = "14px Arial"; + ctx.textAlign = "center"; + ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14); + + + + // ctx.strokeStyle = "#fff"; + // ctx.strokeRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); + + + // ctx.fillStyle = "#000"; + // ctx.fillRect(widgetWidth/2 - border / 2 , widgetY + border / 2 , widgetWidth/2 + border / 2, height + border / 2); + // ctx.fillStyle = this.value; + // ctx.fillRect(widgetWidth/2, widgetY, widgetWidth/2, height); + + } + widget.mouse = function (e, pos, node) { + if (e.type === "pointerdown") { + // get widgets of type type : "COLOR" + const widgets = node.widgets.filter(w => w.type === "COLOR"); + + for (const w of widgets) { + // color picker + const rect = [w.last_y, w.last_y + 32]; + if (pos[1] > rect[0] && pos[1] < rect[1]) { + console.log("color picker", node) + const picker = document.createElement("input"); + picker.type = "color"; + picker.value = this.value; + // picker.style.position = "absolute"; + // picker.style.left = ( pos[0]) + "px"; + // picker.style.top = ( pos[1]) + "px"; + + // place at screen center + picker.style.position = "absolute"; + picker.style.left = "999999px"//(window.innerWidth / 2) + "px"; + picker.style.top = "999999px" //(window.innerHeight / 2) + "px"; + // picker.style.transform = "translate(-50%, -50%)"; + // picker.style.zIndex = 1000; + + + + document.body.appendChild(picker); + + picker.addEventListener("change", () => { + this.value = picker.value; + node.graph._version++; + node.setDirtyCanvas(true, true); + picker.remove(); + }); + + picker.click() + + } + } + } + } + widget.computeSize = function (width) { + return [width, 32]; + } + + return widget; + }, + + DEBUG_IMG: (val, index) => { + const w = { + name: `anything_${index}`, + type: "image", + value: val, + draw: function (ctx, + node, + widgetWidth, + widgetY, + height) { + const [cw, ch] = this.computeSize(widgetWidth) + shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) + }, + computeSize: function (width) { + const ratio = this.inputRatio || 1; + if (width) { + return [width, width / ratio + 4] + } + return [128, 128] + }, + onRemove: function () { + if (this.inputEl) { + this.inputEl.remove(); + } + } + } + + w.inputEl = document.createElement("img"); + w.inputEl.src = w.value; + w.inputEl.onload = function () { + w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight; + } + document.body.appendChild(w.inputEl); + return w + }, + DEBUG_STRING: (val, index) => { + const w = { + name: `anything_${index}`, + type: "debug_text", + val: val, + draw: function (ctx, + node, + widgetWidth, + widgetY, + height) { + // const [cw, ch] = this.computeSize(widgetWidth) + shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) + }, + computeSize: function (width) { + const value = this.inputEl.innerHTML + if (!value) { + return [32, 32] + } + if (!width) { + log(`No width ${this.parent.size}`) + } + + const fontSize = 25; // Assuming 1rem = 16px + + const oldFont = app.ctx.font + app.ctx.font = `${fontSize}px Arial`; + + const words = value.split(" "); + const lines = []; + let currentLine = ""; + for (const word of words) { + const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}`; + + const testWidth = app.ctx.measureText(testLine).width; + + // log(`Testing line ${testLine}, width: ${testWidth}, width: ${width}, ratio: ${testWidth / width}`) + if (testWidth > width) { + lines.push(currentLine); + currentLine = word; + } else { + currentLine = testLine; + } + } + app.ctx.font = oldFont; + lines.push(currentLine); + + // Step 3: Calculate the widget width and height + const textHeight = lines.length * (fontSize + 2); // You can adjust the line height (2 in this case) + const maxLineWidth = lines.reduce((maxWidth, line) => Math.max(maxWidth, app.ctx.measureText(line).width), 0); + const widgetWidth = Math.max(width || this.width || 32, maxLineWidth); + const widgetHeight = textHeight + 10; // Additional padding for spacing + return [widgetWidth, widgetHeight + 4] + + }, + onRemove: function () { + if (this.inputEl) { + this.inputEl.remove(); + } + + } + } + w.inputEl = document.createElement("p"); + w.inputEl.style.textAlign = "center"; + w.inputEl.style.fontSize = "1.5em"; + w.inputEl.style.color = "var(--input-text)"; + w.inputEl.style.fontFamily = "monospace"; + w.inputEl.innerHTML = val + document.body.appendChild(w.inputEl); + + return w } - - widget.desc = "Represents a Bounding Box with x, y, width, and height."; - return widget - } + + + const bboxWidgetDOM = (key, val) => { /** @type {import("./types/litegraph").IWidget} */ const widget = { @@ -266,221 +590,12 @@ const bboxWidgetDOM = (key, val) => { /** * @returns {import("./types/litegraph").IWidget} widget */ -const boolWidget = (key, val, compute = false) => { - /** @type {import("/types/litegraph").IWidget} */ - const widget = { - name: key, - type: "BOOL", - options: { default: false }, - y: 0, - value: val || false, - draw: function (ctx, - node, - widget_width, - widgetY, - height) { - const hide = this.type !== "BOOL" && app.canvas.ds.scale > 0.5 - if (hide) { - return - } - const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; - const background_color = LiteGraph.WIDGET_BGCOLOR; - const text_color = LiteGraph.WIDGET_TEXT_COLOR; - const H = LiteGraph.NODE_WIDGET_HEIGHT; - const arrowSize = 8; - var margin = 15; - if (hide) return; - - var currentY = widgetY; - - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - // ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]); - ctx.rect(margin, currentY, H, H); // Draw checkbox square - - ctx.fill(); - ctx.stroke(); - - ctx.fillStyle = text_color; - // ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7); - ctx.fillText(this.label || this.name, H + margin * 2, currentY + H * 0.7); - - - // Draw arrow if the value is true - // Draw checkmark if the value is true - if (this.value) { - ctx.fillStyle = text_color; - ctx.beginPath(); - ctx.moveTo(margin + H * 0.15, currentY + H * 0.5); - ctx.lineTo(margin + H * 0.4, currentY + H * 0.8); - ctx.lineTo(margin + H * 0.85, currentY + H * 0.2); - ctx.stroke(); - } - - }, - get value() { - - return this.inputEl.value === "true"; - }, - set value(x) { - this.inputEl.value = x; - }, - computeSize: function (width) { - return [width, 32]; - }, - mouse: function (event, pos, node) { - // var x = pos[0] - node.pos[0]; - // var y = pos[1] - node.pos[1]; - // var width = node.size[0]; - // var H = LiteGraph.NODE_WIDGET_HEIGHT; - // var margin = 15; - - // if (event.type == LiteGraph.pointerevents_method + "down") { - // if (x > margin && x < widget_width - margin && y > widgetY && y < widgetY + H) { - // this.value = !this.value; // Toggle checkbox value - // shared.inner_value_change(this, this.value, event); - // app.canvas.setDirty(true); - // } - // } - if (event.type === "pointerdown") { - // get widgets of type type : "COLOR" - const widgets = node.widgets.filter(w => w.type === "BOOL"); - - for (const w of widgets) { - // color picker - const rect = [w.last_y, w.last_y + 32]; - if (pos[1] > rect[0] && pos[1] < rect[1]) { - // picker.style.position = "absolute"; - // picker.style.left = ( pos[0]) + "px"; - // picker.style.top = ( pos[1]) + "px"; - - // place at screen center - // picker.style.position = "absolute"; - // picker.style.left = (window.innerWidth / 2) + "px"; - // picker.style.top = (window.innerHeight / 2) + "px"; - // picker.style.transform = "translate(-50%, -50%)"; - // picker.style.zIndex = 1000; - console.log("Clicked a BOOL", this.value) - - this.value = this.value ? "false" : "true" - - } - } - } - } - } - - // create a checkbox - widget.inputEl = document.createElement("input") - widget.inputEl.type = "checkbox" - widget.inputEl.value = false - document.body.appendChild(widget.inputEl); - return widget - -} /** * @returns {import("./types/litegraph").IWidget} widget */ -const colorWidget = (key, val, compute = false) => { - /** @type {import("/types/litegraph").IWidget} */ - const widget = {} - widget.y = 0 - widget.name = key; - widget.type = "COLOR"; - widget.options = { default: "#ff0000" }; - widget.value = val || "#ff0000"; - widget.draw = function (ctx, - node, - widgetWidth, - widgetY, - height) { - const hide = this.type !== "COLOR" && app.canvas.ds.scale > 0.5 - if (hide) { - return - } - const border = 3; - // draw a rect with a border and a fill color - ctx.fillStyle = "#000"; - ctx.fillRect(0, widgetY, widgetWidth, height); - ctx.fillStyle = this.value; - ctx.fillRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); - // write the input name - // choose the fill based on the luminoisty of this.value color - const color = parseCss(this.value.default || this.value) - if (!color) { - return - } - ctx.fillStyle = shared.isColorBright(color.values, 125) ? "#000" : "#fff"; - - - ctx.font = "14px Arial"; - ctx.textAlign = "center"; - ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14); - - - - // ctx.strokeStyle = "#fff"; - // ctx.strokeRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); - - - // ctx.fillStyle = "#000"; - // ctx.fillRect(widgetWidth/2 - border / 2 , widgetY + border / 2 , widgetWidth/2 + border / 2, height + border / 2); - // ctx.fillStyle = this.value; - // ctx.fillRect(widgetWidth/2, widgetY, widgetWidth/2, height); - - } - widget.mouse = function (e, pos, node) { - if (e.type === "pointerdown") { - // get widgets of type type : "COLOR" - const widgets = node.widgets.filter(w => w.type === "COLOR"); - - for (const w of widgets) { - // color picker - const rect = [w.last_y, w.last_y + 32]; - if (pos[1] > rect[0] && pos[1] < rect[1]) { - console.log("color picker", node) - const picker = document.createElement("input"); - picker.type = "color"; - picker.value = this.value; - // picker.style.position = "absolute"; - // picker.style.left = ( pos[0]) + "px"; - // picker.style.top = ( pos[1]) + "px"; - - // place at screen center - picker.style.position = "absolute"; - picker.style.left = "999999px"//(window.innerWidth / 2) + "px"; - picker.style.top = "999999px" //(window.innerHeight / 2) + "px"; - // picker.style.transform = "translate(-50%, -50%)"; - // picker.style.zIndex = 1000; - - - - document.body.appendChild(picker); - - picker.addEventListener("change", () => { - this.value = picker.value; - node.graph._version++; - node.setDirtyCanvas(true, true); - picker.remove(); - }); - - picker.click() - - } - } - } - } - widget.computeSize = function (width) { - return [width, 32]; - } - - return widget; -} // VIDEO: (node, inputName, inputData, app) => { @@ -591,7 +706,7 @@ const mtb_widgets = { console.debug("Registering bool") return { - widget: node.addCustomWidget(boolWidget(inputName, inputData[1]?.default || false)), + widget: node.addCustomWidget(MtbWidgets.BOOL(inputName, inputData[1]?.default || false)), minWidth: 150, minHeight: 30, }; @@ -600,20 +715,20 @@ const mtb_widgets = { COLOR: (node, inputName, inputData, app) => { console.debug("Registering color") return { - widget: node.addCustomWidget(colorWidget(inputName, inputData[1]?.default || "#ff0000")), + widget: node.addCustomWidget(MtbWidgets.COLOR(inputName, inputData[1]?.default || "#ff0000")), minWidth: 150, minHeight: 30, } }, - BBOX: (node, inputName, inputData, app) => { - console.debug("Registering bbox") - return { - widget: node.addCustomWidget(bboxWidget(inputName, inputData[1]?.default || [0, 0, 0, 0])), - minWidth: 150, - minHeight: 30, - } + // BBOX: (node, inputName, inputData, app) => { + // console.debug("Registering bbox") + // return { + // widget: node.addCustomWidget(MtbWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])), + // minWidth: 150, + // minHeight: 30, + // } - } + // } } }, /** @@ -646,7 +761,6 @@ const mtb_widgets = { for (const [key, input] of Object.entries(rinputs)) { switch (input[0]) { case "COLOR": - console.log(this) // const colW = colorWidget(key, input[1]) // this.addCustomWidget(colW) // const associated_input = this.inputs.findIndex((i) => i.widget?.name === key); @@ -738,12 +852,34 @@ const mtb_widgets = { } break } + case "Save Gif (mtb)": { + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + const r = onExecuted ? onExecuted.apply(this, message) : undefined; + console.log(message) + let imgURLs = [] + if (message && message.gif) { + imgURLs = imgURLs.concat(message.gif.map(params => { + return api.apiURL("/view?" + new URLSearchParams(params).toString()); + })) + console.log(imgURLs) + for (const img of imgURLs) { + const w = this.addCustomWidget(MtbWidgets.DEBUG_IMG(img, 0)) + w.parent = this; + } + } + return r + + } + break + } case "Animation Builder (mtb)": { // console.log(nodeType.prototype) - + const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; this.changeMode(LiteGraph.ALWAYS) // api.addEventListener("executed", ({ detail }) => { @@ -840,11 +976,11 @@ frame: ${this.value % total_frames.value}`; } + return r + } const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (data) { - console.log("executed node", this) onExecuted?.apply(this, data) if (this.widgets) { const pos = this.widgets.findIndex((w) => w.name === "preview"); @@ -874,29 +1010,6 @@ frame: ${this.value % total_frames.value}`; console.debug(`Registered ${nodeType.name} node extra events`) break - } - - case "Debug (mtb)": { - // console.log(`Registered ${nodeType.name} node extra events`) - // nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) { - // // const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined; - // console.debug({ - // type, index, connected, link_info - // }) - - // shared.dynamic_connection(this, index, connected, "anything_", "*") - // } - - // const onNodeCreated = nodeType.prototype.onNodeCreated; - // nodeType.prototype.onNodeCreated = function () { - // const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; - - // this.addInput(`input_${this.inputs.length + 1}`, "*") - - // } - - break - } default: { break From 0950f9914c9bbed7c89f3de33a967cb76f9d0bbb Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 01:19:28 +0200 Subject: [PATCH 33/70] =?UTF-8?q?fix:=20=F0=9F=90=9B=20add=20base64=20pref?= =?UTF-8?q?ix=20to=20outputs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For now I'm using the DEBUG_IMG widget for both Debug and Save Gif. This is done using a DOM element, using the core ui.images freezes the image. --- nodes/debug.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nodes/debug.py b/nodes/debug.py index 75f1fe2..3a95b7a 100644 --- a/nodes/debug.py +++ b/nodes/debug.py @@ -35,7 +35,8 @@ class Debug: buffered = io.BytesIO() im.save(buffered, format="JPEG") b64_imgs.append( - base64.b64encode(buffered.getvalue()).decode("utf-8") + "data:image/jpeg;base64," + + base64.b64encode(buffered.getvalue()).decode("utf-8") ) output["ui"]["b64_images"] += b64_imgs From 4d8ddaca320ce483640d030618e70730b3453df2 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 01:21:46 +0200 Subject: [PATCH 34/70] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20removes?= =?UTF-8?q?=20a=20few=20nodes,=20moved=20other=20around?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/animation.py | 47 ++++++++++ nodes/fun.py | 115 +++++++++--------------- nodes/graph_utils.py | 68 +------------- nodes/image_interpolation.py | 95 +------------------- nodes/io.py | 168 +++++++++++++++++++++++++++++++++++ 5 files changed, 262 insertions(+), 231 deletions(-) create mode 100644 nodes/animation.py create mode 100644 nodes/io.py diff --git a/nodes/animation.py b/nodes/animation.py new file mode 100644 index 0000000..14d564e --- /dev/null +++ b/nodes/animation.py @@ -0,0 +1,47 @@ +from ..log import log + + +class AnimationBuilder: + """Convenient way to manage basic animation maths at the core of many of my workflows""" + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "total_frames": ("INT", {"default": 100, "min": 0}), + # "fps": ("INT", {"default": 12, "min": 0}), + "scale_float": ("FLOAT", {"default": 1.0, "min": 0.0}), + "loop_count": ("INT", {"default": 1, "min": 0}), + "raw_iteration": ("INT", {"default": 0, "min": 0}), + "raw_loop": ("INT", {"default": 0, "min": 0}), + }, + } + + RETURN_TYPES = ("INT", "FLOAT", "INT", "BOOL") + RETURN_NAMES = ("frame", "0-1 (scaled)", "count", "loop_ended") + CATEGORY = "mtb/animation" + FUNCTION = "build_animation" + + def build_animation( + self, + total_frames=100, + # fps=12, + scale_float=1.0, + loop_count=1, # set in js + raw_iteration=0, # set in js + raw_loop=0, # set in js + ): + frame = raw_iteration % (total_frames) + scaled = (frame / (total_frames - 1)) * scale_float + # if frame == 0: + # log.debug("Reseting history") + # PromptServer.instance.prompt_queue.wipe_history() + log.debug(f"frame: {frame}/{total_frames} scaled: {scaled}") + + return (frame, scaled, raw_loop, (frame == (total_frames - 1))) + + +__nodes__ = [AnimationBuilder] diff --git a/nodes/fun.py b/nodes/fun.py index 4835931..5661686 100644 --- a/nodes/fun.py +++ b/nodes/fun.py @@ -1,86 +1,53 @@ import qrcode -from ..utils import pil2tensor, here, tensor2np -from PIL import Image, ImageOps -from functools import lru_cache -import numpy as np -import torch -import hashlib -from pathlib import Path -from ..log import log -import uuid -import folder_paths -import comfy.model_management as model_management -import subprocess +from ..utils import pil2tensor +from PIL import Image +# class MtbExamples: +# """MTB Example Images""" -class SaveGif: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "fps": ("INT", {"default": 12, "min": 1, "max": 120}), - "resize_by": ("FLOAT", {"default": 1.0, "min": 0.1}), - "pingpong": ("BOOL", {"default": False}), - } - } +# def __init__(self): +# pass - RETURN_TYPES = () - OUTPUT_NODE = True - CATEGORY = "mtb/IO" - FUNCTION = "save_gif" +# @classmethod +# @lru_cache(maxsize=1) +# def get_root(cls): +# return here / "examples" / "samples" - def save_gif(self, image, fps=12, resize_by=1.0, pingpong=False): - if image.size(0) == 0: - return ("",) +# @classmethod +# def INPUT_TYPES(cls): +# input_dir = cls.get_root() +# files = [f.name for f in input_dir.iterdir() if f.is_file()] +# return { +# "required": {"image": (sorted(files),)}, +# } - images = tensor2np(image) - images = [frame.astype(np.uint8) for frame in images] - if pingpong: - reversed_frames = images[::-1] - images.extend(reversed_frames) +# RETURN_TYPES = ("IMAGE", "MASK") +# FUNCTION = "do_mtb_examples" +# CATEGORY = "fun" - height, width, _ = image[0].shape +# def do_mtb_examples(self, image, index): +# image_path = (self.get_root() / image).as_posix() - ruuid = uuid.uuid4() - out_path = f"{folder_paths.output_directory}/{ruuid}.gif" +# 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.0 - torch.from_numpy(mask) +# else: +# mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") +# return (image, mask) - log.debug(f"Saving a gif file {width}x{height} as {ruuid}.gif") - - # Prepare the FFmpeg command - command = [ - "ffmpeg", - "-y", - "-f", - "rawvideo", - "-vcodec", - "rawvideo", - "-s", - f"{width}x{height}", - "-pix_fmt", - "rgb24", # GIF only supports rgb24 - "-r", - str(fps), - "-i", - "-", - "-vf", - f"fps={fps},scale={width * resize_by}:-1", # Set frame rate and resize if necessary - "-y", - out_path, - ] - - process = subprocess.Popen(command, stdin=subprocess.PIPE) - - for frame in images: - model_management.throw_exception_if_processing_interrupted() - process.stdin.write(frame.tobytes()) - - process.stdin.close() - process.wait() - results = [] - results.append({"filename": f"{ruuid}.gif", "subfolder": "", "type": "output"}) - return {"ui": {"images": results}} +# @classmethod +# def IS_CHANGED(cls, image): +# image_path = (cls.get_root() / image).as_posix() +# m = hashlib.sha256() +# with open(image_path, "rb") as f: +# m.update(f.read()) +# return m.digest().hex() class QrCode: @@ -145,5 +112,5 @@ class QrCode: __nodes__ = [ QrCode, - SaveGif, + # MtbExamples, ] diff --git a/nodes/graph_utils.py b/nodes/graph_utils.py index e514c42..e191fbc 100644 --- a/nodes/graph_utils.py +++ b/nodes/graph_utils.py @@ -1,70 +1,6 @@ -import torch -import folder_paths -import os from ..log import log -class SaveTensors: - """Debug node that will probably be removed in the future""" - - def __init__(self): - self.output_dir = folder_paths.get_output_directory() - self.type = "mtb/debug" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "filename_prefix": ("STRING", {"default": "ComfyPickle"}), - }, - "optional": { - "image": ("IMAGE",), - "mask": ("MASK",), - "latent": ("LATENT",), - }, - } - - FUNCTION = "save" - OUTPUT_NODE = True - RETURN_TYPES = () - CATEGORY = "mtb/debug" - - def save( - self, - filename_prefix, - image: torch.Tensor = None, - mask: torch.Tensor = None, - latent: torch.Tensor = None, - ): - ( - full_output_folder, - filename, - counter, - subfolder, - filename_prefix, - ) = folder_paths.get_save_image_path(filename_prefix, self.output_dir) - - if image is not None: - image_file = f"{filename}_image_{counter:05}.pt" - torch.save(image, os.path.join(full_output_folder, image_file)) - # np.save(os.path.join(full_output_folder, image_file), image.cpu().numpy()) - - if mask is not None: - mask_file = f"{filename}_mask_{counter:05}.pt" - torch.save(mask, os.path.join(full_output_folder, mask_file)) - # np.save(os.path.join(full_output_folder, mask_file), mask.cpu().numpy()) - - if latent is not None: - # for latent we must use pickle - latent_file = f"{filename}_latent_{counter:05}.pt" - torch.save(latent, os.path.join(full_output_folder, latent_file)) - # pickle.dump(latent, open(os.path.join(full_output_folder, latent_file), "wb")) - - # np.save(os.path.join(full_output_folder, latent_file), latent[""].cpu().numpy()) - - return f"{filename_prefix}_{counter:05}" - - class StringReplace: """Basic string replacement""" @@ -95,6 +31,8 @@ class StringReplace: class FitNumber: + """Fit the input float using a source and target range""" + @classmethod def INPUT_TYPES(cls): return { @@ -134,4 +72,4 @@ class FitNumber: return (res,) -__nodes__ = [SaveTensors, StringReplace, FitNumber] +__nodes__ = [StringReplace, FitNumber] diff --git a/nodes/image_interpolation.py b/nodes/image_interpolation.py index 5ac882f..aa91a32 100644 --- a/nodes/image_interpolation.py +++ b/nodes/image_interpolation.py @@ -7,9 +7,7 @@ from ..log import log import torch from frame_interpolation.eval import util, interpolator from ..utils import tensor2np -import uuid import numpy as np -import subprocess import comfy from PIL import Image import urllib.request @@ -33,9 +31,9 @@ def get_image(filename, subfolder, folder_type): class GetBatchFromHistory: - """Experimental node to load images from the history of the server. + """Very experimental node to load images from the history of the server. - Queue item without output are ignore in the count.""" + Queue items without output are ignore in the count.""" @classmethod def INPUT_TYPES(cls): @@ -62,10 +60,9 @@ class GetBatchFromHistory: log.debug("Load from history is disabled for this iteration") return (torch.zeros(0),) frames = [] - server_address = "localhost:3000" with urllib.request.urlopen( - "http://{}/history".format(server_address) + "http://{}:{}/history".format(args.listen, args.port) ) as response: history = json.loads(response.read()) @@ -250,95 +247,9 @@ class ConcatImages: return (self.concatenate_tensors(imageA, imageB),) -class ExportToProres: - """Export to ProRes 4444 (Experimental)""" - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "images": ("IMAGE",), - # "frames": ("FRAMES",), - "fps": ("FLOAT", {"default": 24, "min": 1}), - "prefix": ("STRING", {"default": "export"}), - } - } - - RETURN_TYPES = ("VIDEO",) - OUTPUT_NODE = True - FUNCTION = "export_prores" - CATEGORY = "mtb/IO" - - def export_prores( - self, - images: torch.Tensor, - fps: float, - prefix: str, - ): - if images.size(0) == 0: - return ("",) - output_dir = Path(folder_paths.get_output_directory()) - id = f"{prefix}_{uuid.uuid4()}.mov" - - log.debug(f"Exporting to {output_dir / id}") - - frames = tensor2np(images) - log.debug(f"Frames type {type(frames[0])}") - log.debug(f"Exporting {len(frames)} frames") - - frames = [frame.astype(np.uint16) * 257 for frame in frames] - - height, width, _ = frames[0].shape - - out_path = (output_dir / id).as_posix() - - # Prepare the FFmpeg command - command = [ - "ffmpeg", - "-y", - "-f", - "rawvideo", - "-vcodec", - "rawvideo", - "-s", - f"{width}x{height}", - "-pix_fmt", - "rgb48le", - "-r", - str(fps), - "-i", - "-", - "-c:v", - "prores_ks", - "-profile:v", - "4", - "-pix_fmt", - "yuva444p10le", - "-r", - str(fps), - "-y", - out_path, - ] - - process = subprocess.Popen(command, stdin=subprocess.PIPE) - - for frame in frames: - model_management.throw_exception_if_processing_interrupted() - process.stdin.write(frame.tobytes()) - - process.stdin.close() - process.wait() - - return (out_path,) - - __nodes__ = [ LoadFilmModel, FilmInterpolation, - ExportToProres, ConcatImages, GetBatchFromHistory, ] diff --git a/nodes/io.py b/nodes/io.py new file mode 100644 index 0000000..e102d7e --- /dev/null +++ b/nodes/io.py @@ -0,0 +1,168 @@ +from ..utils import tensor2np +import uuid +import folder_paths +from ..log import log +import comfy.model_management as model_management +import subprocess +import torch +from pathlib import Path +import numpy as np + + +class ExportToProres: + """Export to ProRes 4444 (Experimental)""" + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + # "frames": ("FRAMES",), + "fps": ("FLOAT", {"default": 24, "min": 1}), + "prefix": ("STRING", {"default": "export"}), + } + } + + RETURN_TYPES = ("VIDEO",) + OUTPUT_NODE = True + FUNCTION = "export_prores" + CATEGORY = "mtb/IO" + + def export_prores( + self, + images: torch.Tensor, + fps: float, + prefix: str, + ): + if images.size(0) == 0: + return ("",) + output_dir = Path(folder_paths.get_output_directory()) + id = f"{prefix}_{uuid.uuid4()}.mov" + + log.debug(f"Exporting to {output_dir / id}") + + frames = tensor2np(images) + log.debug(f"Frames type {type(frames[0])}") + log.debug(f"Exporting {len(frames)} frames") + + frames = [frame.astype(np.uint16) * 257 for frame in frames] + + height, width, _ = frames[0].shape + + out_path = (output_dir / id).as_posix() + + # Prepare the FFmpeg command + command = [ + "ffmpeg", + "-y", + "-f", + "rawvideo", + "-vcodec", + "rawvideo", + "-s", + f"{width}x{height}", + "-pix_fmt", + "rgb48le", + "-r", + str(fps), + "-i", + "-", + "-c:v", + "prores_ks", + "-profile:v", + "4", + "-pix_fmt", + "yuva444p10le", + "-r", + str(fps), + "-y", + out_path, + ] + + process = subprocess.Popen(command, stdin=subprocess.PIPE) + + for frame in frames: + model_management.throw_exception_if_processing_interrupted() + process.stdin.write(frame.tobytes()) + + process.stdin.close() + process.wait() + + return (out_path,) + + +class SaveGif: + """Save the images from the batch as a GIF""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "fps": ("INT", {"default": 12, "min": 1, "max": 120}), + "resize_by": ("FLOAT", {"default": 1.0, "min": 0.1}), + "pingpong": ("BOOL", {"default": False}), + } + } + + RETURN_TYPES = () + OUTPUT_NODE = True + CATEGORY = "mtb/IO" + FUNCTION = "save_gif" + + def save_gif(self, image, fps=12, resize_by=1.0, pingpong=False): + if image.size(0) == 0: + return ("",) + + images = tensor2np(image) + images = [frame.astype(np.uint8) for frame in images] + if pingpong: + reversed_frames = images[::-1] + images.extend(reversed_frames) + + height, width, _ = image[0].shape + + ruuid = uuid.uuid4() + out_path = f"{folder_paths.output_directory}/{ruuid}.gif" + + log.debug(f"Saving a gif file {width}x{height} as {ruuid}.gif") + + # Prepare the FFmpeg command + command = [ + "ffmpeg", + "-y", + "-f", + "rawvideo", + "-vcodec", + "rawvideo", + "-s", + f"{width}x{height}", + "-pix_fmt", + "rgb24", # GIF only supports rgb24 + "-r", + str(fps), + "-i", + "-", + "-vf", + f"fps={fps},scale={width * resize_by}:-1", # Set frame rate and resize if necessary + "-y", + out_path, + ] + + process = subprocess.Popen(command, stdin=subprocess.PIPE) + + for frame in images: + model_management.throw_exception_if_processing_interrupted() + process.stdin.write(frame.tobytes()) + + process.stdin.close() + process.wait() + results = [] + results.append({"filename": f"{ruuid}.gif", "subfolder": "", "type": "output"}) + return {"ui": {"gif": results}} + + +__nodes__ = [SaveGif, ExportToProres] From 18402e3be1ab47e10109cfd2dff18863a1ee56f7 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 01:22:24 +0200 Subject: [PATCH 35/70] =?UTF-8?q?fix:=20=F0=9F=9A=91=EF=B8=8F=20set=20debu?= =?UTF-8?q?g=20level=20from=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 4660d6a..89fd22c 100644 --- a/__init__.py +++ b/__init__.py @@ -139,9 +139,10 @@ log.info( # - ENDPOINT from server import PromptServer -from .log import mklog +from .log import mklog, log from aiohttp import web from importlib import reload +import logging endlog = mklog("endpoint") @@ -164,10 +165,15 @@ async def set_debug(request): enabled = json_data.get("enabled") if enabled: os.environ["MTB_DEBUG"] = "true" + log.setLevel(logging.DEBUG) + log.debug("Debug mode set") + else: if "MTB_DEBUG" in os.environ: # del os.environ["MTB_DEBUG"] os.environ.pop("MTB_DEBUG") + log.setLevel(logging.INFO) + return web.json_response({"message": f"Debug mode {'set' if enabled else 'unset'}"}) From c794d6a071778220d654b526d2edfddcc79752fc Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 01:27:40 +0200 Subject: [PATCH 36/70] =?UTF-8?q?fix:=20=E2=9A=A1=EF=B8=8F=20update=20node?= =?UTF-8?q?=5Flist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cc @ltdrdata 👀 --- node_list.json | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/node_list.json b/node_list.json index 422826f..42dc354 100644 --- a/node_list.json +++ b/node_list.json @@ -1,20 +1,20 @@ { - "B Box From Mask (mtb)": "From a mask extract the bounding box", + "Animation Builder (mtb)": "Convenient way to manage basic animation maths at the core of many of my workflows", + "Bbox (mtb)": "The bounding box (BBOX) custom type used by other nodes", + "Bbox From Mask (mtb)": "From a mask extract the bounding box", "Blur (mtb)": "Blur an image using a Gaussian filter.", - "Bounding Box (mtb)": "The bounding box (BBOX) custom type used by other nodes", "Color Correct (mtb)": "Various color correction methods", "Colored Image (mtb)": "Constant color image of given size", "Concat Images (mtb)": "Add images to batch", "Crop (mtb)": "Crops an image and an optional mask to a given bounding box\n\n The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type\n The BBOX input takes precedence over the tuple input\n ", + "Debug (mtb)": "Experimental node to debug any Comfy values, support for more types and widgets is planned", "Deep Bump (mtb)": "Normal & height maps generation from single pictures", - "Deglaze Image (mtb)": "Remove adversarial noise from images", - "Denoise (mtb)": "Denoise an image using total variation minimization.", "Export To Prores (mtb)": "Export to ProRes 4444 (Experimental)", "Face Swap (mtb)": "Face swap using deepinsight/insightface models", "Film Interpolation (mtb)": "Google Research FILM frame interpolation for large motion", + "Fit Number (mtb)": "Fit the input float using a source and target range", "Float To Number (mtb)": "Node addon for the WAS Suite. Converts a \"comfy\" FLOAT to a NUMBER.", - "Get Batch From History (mtb)": "Experimental node to load images from the history of the server.\n\n Queue item without output are ignore in the count.", - "Hsv To Rgb (mtb)": "Convert HSV image to RGB", + "Get Batch From History (mtb)": "Very experimental node to load images from the history of the server.\n\n Queue items without output are ignore in the count.", "Image Compare (mtb)": "Compare two images and return a difference image", "Image Premultiply (mtb)": "Premultiply image with mask", "Image Remove Background Rembg (mtb)": "Removes the background from the input using Rembg.", @@ -30,13 +30,11 @@ "Load Image From Url (mtb)": "Load an image from the given URL", "Load Image Sequence (mtb)": "Load an image sequence from a folder. The current frame is used to determine which image to load.\n\n Usually used in conjunction with the `Primitive` node set to increment to load a sequence of images from a folder.\n Use -1 to load all matching frames as a batch.\n ", "Mask To Image (mtb)": "Converts a mask (alpha) to an RGB image with a color and background", - "Mtb Examples (mtb)": "MTB Example Images", "Qr Code (mtb)": "Basic QR Code generator", "Restore Face (mtb)": "Uses GFPGan to restore faces", - "Rgb To Hsv (mtb)": "Convert RGB image to HSV", + "Save Gif (mtb)": "Save the images from the batch as a GIF", "Save Image Grid (mtb)": "Save all the images in the input batch as a grid of images.", "Save Image Sequence (mtb)": "Save an image sequence to a folder. The current frame is used to determine which image to save.\n\n This is merely a wrapper around the `save_images` function with formatting for the output folder and filename.\n ", - "Save Tensors (mtb)": "Debug node that will probably be removed in the future", "Smart Step (mtb)": "Utils to control the steps start/stop of the KAdvancedSampler in percentage", "String Replace (mtb)": "Basic string replacement", "Styles Loader (mtb)": "Load csv files and populate a dropdown from the rows (\u00e0 la A111)", From 4e6b87719989aa144946c5c9a43b9398c20bf11e Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 01:29:58 +0200 Subject: [PATCH 37/70] =?UTF-8?q?fix:=20=F0=9F=8E=89=20remove=20tests=20&?= =?UTF-8?q?=20add=20missing=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/crop.py | 15 +-------------- nodes/debug.py | 2 ++ 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/nodes/crop.py b/nodes/crop.py index 96bd71e..fc7751a 100644 --- a/nodes/crop.py +++ b/nodes/crop.py @@ -6,19 +6,6 @@ import numpy as np from ..log import log -class BoolInput: - @classmethod - def INPUT_TYPES(cls): - return {"required": {"bool": ("BOOL",)}} - - RETURN_TYPES = ("BOOL",) - FUNCTION = "do_bool" - CATEGORY = "mtb/test" - - def do_bool(self, bool): - return (bool,) - - class Bbox: """The bounding box (BBOX) custom type used by other nodes""" @@ -310,4 +297,4 @@ class Uncrop: return (pil2tensor(out_images),) -__nodes__ = [BboxFromMask, Bbox, Crop, Uncrop, BoolInput] +__nodes__ = [BboxFromMask, Bbox, Crop, Uncrop] diff --git a/nodes/debug.py b/nodes/debug.py index 3a95b7a..ba6bdb9 100644 --- a/nodes/debug.py +++ b/nodes/debug.py @@ -5,6 +5,8 @@ import torch class Debug: + """Experimental node to debug any Comfy values, support for more types and widgets is planned""" + @classmethod def INPUT_TYPES(cls): return { From dad3966ba219c1998e4fc7f6e641864fb0e7c3e8 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 03:25:42 +0200 Subject: [PATCH 38/70] =?UTF-8?q?feat:=20=F0=9F=9A=80=20add=20install=20sc?= =?UTF-8?q?ript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit still needs testing --- .gitignore | 1 + install.py | 382 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 383 insertions(+) create mode 100644 install.py diff --git a/.gitignore b/.gitignore index 80053fe..d5bec60 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__ *.py[cod] *.onnx +wheels/ node_modules/ \ No newline at end of file diff --git a/install.py b/install.py new file mode 100644 index 0000000..2104c77 --- /dev/null +++ b/install.py @@ -0,0 +1,382 @@ +import requests +import os +import ast +import re +import argparse +import sys +import subprocess +from importlib import import_module +import platform +from pathlib import Path +import sys +import zipfile +import shutil + + +here = Path(__file__).parent +wheels_directory = here / "wheels" +# region ansi +# ANSI escape sequences for text styling +ANSI_FORMATS = { + "reset": "\033[0m", + "bold": "\033[1m", + "dim": "\033[2m", + "italic": "\033[3m", + "underline": "\033[4m", + "blink": "\033[5m", + "reverse": "\033[7m", + "strike": "\033[9m", +} + +ANSI_COLORS = { + "black": "\033[30m", + "red": "\033[31m", + "green": "\033[32m", + "yellow": "\033[33m", + "blue": "\033[34m", + "magenta": "\033[35m", + "cyan": "\033[36m", + "white": "\033[37m", + "bright_black": "\033[30;1m", + "bright_red": "\033[31;1m", + "bright_green": "\033[32;1m", + "bright_yellow": "\033[33;1m", + "bright_blue": "\033[34;1m", + "bright_magenta": "\033[35;1m", + "bright_cyan": "\033[36;1m", + "bright_white": "\033[37;1m", + "bg_black": "\033[40m", + "bg_red": "\033[41m", + "bg_green": "\033[42m", + "bg_yellow": "\033[43m", + "bg_blue": "\033[44m", + "bg_magenta": "\033[45m", + "bg_cyan": "\033[46m", + "bg_white": "\033[47m", + "bg_bright_black": "\033[40;1m", + "bg_bright_red": "\033[41;1m", + "bg_bright_green": "\033[42;1m", + "bg_bright_yellow": "\033[43;1m", + "bg_bright_blue": "\033[44;1m", + "bg_bright_magenta": "\033[45;1m", + "bg_bright_cyan": "\033[46;1m", + "bg_bright_white": "\033[47;1m", +} + + +def apply_format(text, *formats): + """Apply ANSI escape sequences for the specified formats to the given text.""" + formatted_text = text + for format in formats: + formatted_text = f"{ANSI_FORMATS.get(format, '')}{formatted_text}{ANSI_FORMATS.get('reset', '')}" + return formatted_text + + +def apply_color(text, color=None, background=None): + """Apply ANSI escape sequences for the specified color and background to the given text.""" + formatted_text = text + if color: + formatted_text = f"{ANSI_COLORS.get(color, '')}{formatted_text}{ANSI_FORMATS.get('reset', '')}" + if background: + formatted_text = f"{ANSI_COLORS.get(background, '')}{formatted_text}{ANSI_FORMATS.get('reset', '')}" + return formatted_text + + +def print_formatted(text, *formats, color=None, background=None, **kwargs): + """Print the given text with the specified formats, color, and background.""" + formatted_text = apply_format(text, *formats) + formatted_text = apply_color(formatted_text, color, background) + file = kwargs.get("file", sys.stdout) + print(formatted_text, file=file) + + +# endregion + +try: + import requirements +except ImportError: + print_formatted("Installing requirements-parser...", "italic", color="yellow") + subprocess.check_call( + [sys.executable, "-m", "pip", "install", "requirements-parser"] + ) + import requirements + + print_formatted("Done.", "italic", color="green") + +try: + from tqdm import tqdm +except ImportError: + print_formatted("Installing tqdm...", "italic", color="yellow") + subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "tqdm"]) + from tqdm import tqdm + + +pip_map = { + "onnxruntime-gpu": "onnxruntime", + "opencv-contrib": "cv2", + "tb-nightly": "tensorboard", + "protobuf": "google.protobuf", + # Add more mappings as needed +} + + +# Get the version from __init__.py +def get_local_version(): + init_file = os.path.join(os.path.dirname(__file__), "__init__.py") + if os.path.isfile(init_file): + with open(init_file, "r") as f: + tree = ast.parse(f.read()) + for node in ast.walk(tree): + if isinstance(node, ast.Assign): + for target in node.targets: + if ( + isinstance(target, ast.Name) + and target.id == "__version__" + and isinstance(node.value, ast.Str) + ): + return node.value.s + return None + + +def download_file(url, file_name): + with requests.get(url, stream=True) as response: + response.raise_for_status() + total_size = int(response.headers.get("content-length", 0)) + with open(file_name, "wb") as file, tqdm( + desc=file_name.stem, + total=total_size, + unit="B", + unit_scale=True, + unit_divisor=1024, + ) as progress_bar: + for chunk in response.iter_content(chunk_size=8192): + file.write(chunk) + progress_bar.update(len(chunk)) + + +from typing import Union, Literal + + +def detect_mode() -> ( + Union[Literal["colab"], Literal["embeded"] | Literal["venv"], None] +): + if os.environ.get("COLAB_GPU"): + return "colab" + executable = sys.executable + if "python_embeded" in executable: + return "embeded" + if ".venv" in executable: + return "venv" + + +# Install dependencies from requirements.txt +def install_dependencies(dry=False): + with open("requirements.txt", "r") as requirements_file: + requirements_txt = requirements_file.read() + + try: + parsed_requirements = requirements.parse(requirements_txt) + except AttributeError: + print_formatted( + "Failed to parse requirements.txt. Please make sure the file is correctly formatted.", + "bold", + color="red", + ) + + return + + print_formatted( + "Installing dependencies from requirements.txt...", "italic", color="yellow" + ) + + for requirement in parsed_requirements: + dependency = requirement.name.strip() + import_name = pip_map.get(dependency, dependency) + + pip_name = dependency + if specs := requirement.specs: + pip_name += "".join(specs[0]) + + try: + import_module(import_name) + print_formatted( + f"Package {pip_name} already installed (import name: '{import_name}').", + "bold", + color="green", + ) + except ImportError: + if dry: + print_formatted( + f"Dry-run: Package {pip_name} would be installed (import name: '{import_name}').", + color="yellow", + ) + else: + try: + subprocess.check_call( + [sys.executable, "-m", "pip", "install", pip_name] + ) + print_formatted( + f"Package {pip_name} installed successfully using pip package name (import name: '{import_name}')", + "bold", + color="green", + ) + except subprocess.CalledProcessError as e: + print_formatted( + f"Failed to install package {pip_name} using pip package name (import name: '{import_name}'). Error: {str(e)}", + "bold", + color="red", + ) + + +if __name__ == "__main__": + # Parse command-line arguments + parser = argparse.ArgumentParser() + parser.add_argument( + "--wheels", "-w", action="store_true", help="Install wheel dependencies" + ) + parser.add_argument( + "--requirements", "-r", action="store_true", help="Install requirements.txt" + ) + parser.add_argument( + "--dry", + action="store_true", + help="Print what will happen without doing it (still making requests to the GH Api)", + ) + + parser.add_argument( + "--version", + default=get_local_version(), + help="Version to check against the GitHub API", + ) + + args = parser.parse_args() + + # Install dependencies from requirements.txt + if args.requirements: + install_dependencies(dry=args.dry) + + mode = detect_mode() + + if not args.wheels and mode not in ["colab", "embeded"]: + print_formatted( + "Skipping wheel installation. Use --wheels to install wheel dependencies. (only needed for Comfy embed)", + "italic", + color="yellow", + ) + sys.exit() + + if mode in ["colab", "embeded"]: + print( + f"Downloading and installing release wheels since we are in a Comfy {apply_color(mode,'cyan')} environment", + ) + # Fetch the JSON data from the GitHub API URL + owner = "melmass" + repo = "comfy_mtb" + version = args.version + current_platform = platform.system().lower() + + # Get the tag version from the GitHub API + tag_url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/v{version}" + response = requests.get(tag_url) + if response.status_code == 404: + print_formatted( + f"Tag version '{apply_color(version,'cyan')}' not found for {owner}/{repo} repository." + ) + sys.exit() + + tag_data = response.json() + tag_name = tag_data["name"] + + # # Compare the local and tag versions + # if version and tag_name: + # if re.match(r"v?(\d+(\.\d+)+)", version) and re.match( + # r"v?(\d+(\.\d+)+)", tag_name + # ): + # version_parts = [int(part) for part in version.lstrip("v").split(".")] + # tag_version_parts = [int(part) for part in tag_name.lstrip("v").split(".")] + + # if version_parts > tag_version_parts: + # print_formatted( + # f"Local version ({version}) is greater than the release version ({tag_name}).", + # "bold", + # "yellow", + # ) + # sys.exit() + + # Download the assets for the given version + matching_assets = [ + asset for asset in tag_data["assets"] if current_platform in asset["name"] + ] + if not matching_assets: + print(f"Unsupported operating system: {current_platform}") + + wheels_directory.mkdir(exist_ok=True) + + for asset in matching_assets: + asset_name = asset["name"] + asset_download_url = asset["browser_download_url"] + print_formatted(f"Downloading asset: {asset_name}", color="yellow") + asset_dest = wheels_directory / asset_name + if asset_dest.exists(): + print_formatted( + f"Asset '{asset_dest}' already exists. Skipping.", + "italic", + color="yellow", + ) + continue + download_file(asset_download_url, asset_dest) + + # - Unzip to wheels dir + whl_files = [] + with zipfile.ZipFile(asset_dest, "r") as zip_ref: + for item in tqdm(zip_ref.namelist(), desc="Extracting", unit="file"): + if item.endswith(".whl"): + item_basename = os.path.basename(item) + target_path = wheels_directory / item_basename + with zip_ref.open(item) as source, open( + target_path, "wb" + ) as target: + whl_files.append(target_path) + shutil.copyfileobj(source, target) + + print_formatted( + f"Wheels extracted for {current_platform} to the '{wheels_directory}' directory.", + "bold", + color="green", + ) + + if whl_files: + for whl_file in tqdm(whl_files, desc="Installing", unit="package"): + whl_path = wheels_directory / whl_file + + # check if installed + try: + whl_dep = whl_path.name.split("-")[0] + import_name = pip_map.get(whl_dep, whl_dep) + import_module(import_name) + tqdm.write( + f"Package {import_name} already installed, skipping wheel installation.", + ) + continue + except ImportError: + if args.dry: + tqdm.write( + f"Dry-run: Package {whl_path.name} would be installed.", + ) + continue + + tqdm.write("Installing wheel: " + whl_path.name) + + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + whl_path.resolve().as_posix(), + ] + ) + + print_formatted("Wheels installation completed.", color="green") + else: + print_formatted("No .whl files found. Nothing to install.", color="yellow") From 7e301e2a067d41cba9b8ef357496dd1df94e4cdd Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 03:45:05 +0200 Subject: [PATCH 39/70] =?UTF-8?q?fix:=20=F0=9F=90=9B=20install=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- install.py | 131 ++++++++++++++++++++++++++--------------------- requirements.txt | 3 -- 2 files changed, 74 insertions(+), 60 deletions(-) diff --git a/install.py b/install.py index 2104c77..c67570e 100644 --- a/install.py +++ b/install.py @@ -15,6 +15,21 @@ import shutil here = Path(__file__).parent wheels_directory = here / "wheels" +executable = sys.executable + +# - detect mode +mode = None +if os.environ.get("COLAB_GPU"): + mode = "colab" +elif "python_embeded" in executable: + mode = "embeded" +elif ".venv" in executable: + mode = "venv" + + +if mode == None: + mode = "unknown" + # region ansi # ANSI escape sequences for text styling ANSI_FORMATS = { @@ -109,6 +124,7 @@ except ImportError: print_formatted("Installing tqdm...", "italic", color="yellow") subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "tqdm"]) from tqdm import tqdm +import importlib pip_map = { @@ -154,78 +170,81 @@ def download_file(url, file_name): progress_bar.update(len(chunk)) -from typing import Union, Literal - - -def detect_mode() -> ( - Union[Literal["colab"], Literal["embeded"] | Literal["venv"], None] -): - if os.environ.get("COLAB_GPU"): - return "colab" - executable = sys.executable - if "python_embeded" in executable: - return "embeded" - if ".venv" in executable: - return "venv" - - -# Install dependencies from requirements.txt -def install_dependencies(dry=False): - with open("requirements.txt", "r") as requirements_file: +def get_requirements(path): + with open(path, "r") as requirements_file: requirements_txt = requirements_file.read() try: parsed_requirements = requirements.parse(requirements_txt) except AttributeError: print_formatted( - "Failed to parse requirements.txt. Please make sure the file is correctly formatted.", + f"Failed to parse {path}. Please make sure the file is correctly formatted.", "bold", color="red", ) return + return parsed_requirements + + +def import_or_install(requirement, dry=False): + dependency = requirement.name.strip() + import_name = pip_map.get(dependency, dependency) + + pip_name = dependency + if specs := requirement.specs: + pip_name += "".join(specs[0]) + + try: + import_module(import_name) + print_formatted( + f"Package {pip_name} already installed (import name: '{import_name}').", + "bold", + color="green", + ) + except ImportError: + if dry: + print_formatted( + f"Dry-run: Package {pip_name} would be installed (import name: '{import_name}').", + color="yellow", + ) + else: + try: + subprocess.check_call( + [sys.executable, "-m", "pip", "install", pip_name] + ) + print_formatted( + f"Package {pip_name} installed successfully using pip package name (import name: '{import_name}')", + "bold", + color="green", + ) + except subprocess.CalledProcessError as e: + print_formatted( + f"Failed to install package {pip_name} using pip package name (import name: '{import_name}'). Error: {str(e)}", + "bold", + color="red", + ) + + +# Install dependencies from requirements.txt +def install_dependencies(dry=False): + parsed_requirements = get_requirements(here / "requirements.txt") + if not parsed_requirements: + return print_formatted( "Installing dependencies from requirements.txt...", "italic", color="yellow" ) for requirement in parsed_requirements: - dependency = requirement.name.strip() - import_name = pip_map.get(dependency, dependency) + import_or_install(requirement, dry=dry) - pip_name = dependency - if specs := requirement.specs: - pip_name += "".join(specs[0]) - - try: - import_module(import_name) - print_formatted( - f"Package {pip_name} already installed (import name: '{import_name}').", - "bold", - color="green", - ) - except ImportError: - if dry: - print_formatted( - f"Dry-run: Package {pip_name} would be installed (import name: '{import_name}').", - color="yellow", - ) - else: - try: - subprocess.check_call( - [sys.executable, "-m", "pip", "install", pip_name] - ) - print_formatted( - f"Package {pip_name} installed successfully using pip package name (import name: '{import_name}')", - "bold", - color="green", - ) - except subprocess.CalledProcessError as e: - print_formatted( - f"Failed to install package {pip_name} using pip package name (import name: '{import_name}'). Error: {str(e)}", - "bold", - color="red", - ) + if mode == "venv": + parsed_requirements = get_requirements(here / "requirements-wheels.txt") + if not parsed_requirements: + return + for requirement in parsed_requirements: + import_or_install(requirement, dry=dry) if __name__ == "__main__": @@ -252,11 +271,9 @@ if __name__ == "__main__": args = parser.parse_args() # Install dependencies from requirements.txt - if args.requirements: + if args.requirements or mode == "venv": install_dependencies(dry=args.dry) - mode = detect_mode() - if not args.wheels and mode not in ["colab", "embeded"]: print_formatted( "Skipping wheel installation. Use --wheels to install wheel dependencies. (only needed for Comfy embed)", diff --git a/requirements.txt b/requirements.txt index abb3097..ce1906f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,6 @@ onnxruntime-gpu==1.15.1 imageio===2.28.1 qrcode[pil] numpy==1.23.5 -insightface==0.7.3 -mmcv==2.0.0 -mmdet==3.0.0 rembg==2.0.37 facexlib==0.3.0 basicsr==1.4.2 From fccf31348994ab6e344a1ab00a8f9998309f9319 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 04:02:36 +0200 Subject: [PATCH 40/70] =?UTF-8?q?fix:=20=F0=9F=9A=91=EF=B8=8F=20always=20u?= =?UTF-8?q?se=20latest=20for=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit more simple, also double check deps there, it's not a big deal and should solve issues, tested in colab --- install.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/install.py b/install.py index c67570e..7544842 100644 --- a/install.py +++ b/install.py @@ -271,8 +271,8 @@ if __name__ == "__main__": args = parser.parse_args() # Install dependencies from requirements.txt - if args.requirements or mode == "venv": - install_dependencies(dry=args.dry) + # if args.requirements or mode == "venv": + install_dependencies(dry=args.dry) if not args.wheels and mode not in ["colab", "embeded"]: print_formatted( @@ -289,16 +289,17 @@ if __name__ == "__main__": # Fetch the JSON data from the GitHub API URL owner = "melmass" repo = "comfy_mtb" - version = args.version + # version = args.version current_platform = platform.system().lower() # Get the tag version from the GitHub API - tag_url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/v{version}" + tag_url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" response = requests.get(tag_url) if response.status_code == 404: - print_formatted( - f"Tag version '{apply_color(version,'cyan')}' not found for {owner}/{repo} repository." - ) + # print_formatted( + # f"Tag version '{apply_color(version,'cyan')}' not found for {owner}/{repo} repository." + # ) + print_formatted("Error retrieving the release assets.", color="red") sys.exit() tag_data = response.json() From 7335003346e83666c5dee631b8e6b15586d871e7 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 04:56:10 +0200 Subject: [PATCH 41/70] =?UTF-8?q?fix:=20=F0=9F=92=84=20save=20gif=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/io.py | 3 +++ web/mtb_widgets.js | 56 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/nodes/io.py b/nodes/io.py index e102d7e..873d284 100644 --- a/nodes/io.py +++ b/nodes/io.py @@ -126,6 +126,9 @@ class SaveGif: height, width, _ = image[0].shape ruuid = uuid.uuid4() + + ruuid = ruuid.hex[:10] + out_path = f"{folder_paths.output_directory}/{ruuid}.gif" log.debug(f"Saving a gif file {width}x{height} as {ruuid}.gif") diff --git a/web/mtb_widgets.js b/web/mtb_widgets.js index 97927b3..3261636 100644 --- a/web/mtb_widgets.js +++ b/web/mtb_widgets.js @@ -857,20 +857,52 @@ const mtb_widgets = { nodeType.prototype.onExecuted = function (message) { const r = onExecuted ? onExecuted.apply(this, message) : undefined; console.log(message) - let imgURLs = [] - if (message && message.gif) { - imgURLs = imgURLs.concat(message.gif.map(params => { - return api.apiURL("/view?" + new URLSearchParams(params).toString()); - })) - console.log(imgURLs) - for (const img of imgURLs) { - const w = this.addCustomWidget(MtbWidgets.DEBUG_IMG(img, 0)) - w.parent = this; - } - } - return r + if (this.widgets) { + const pos = this.widgets.findIndex((w) => w.name === "anything_0"); + if (pos !== -1) { + for (let i = pos; i < this.widgets.length; i++) { + console.log(this.widgets[i]) + console.log(this) + this.widgets[i].onRemove?.(); + } + this.widgets.length = pos + + + } + + let imgURLs = [] + if (message && message.gif) { + imgURLs = imgURLs.concat(message.gif.map(params => { + return api.apiURL("/view?" + new URLSearchParams(params).toString()); + })) + console.log(imgURLs) + for (const img of imgURLs) { + const w = this.addCustomWidget(MtbWidgets.DEBUG_IMG(img, 0)) + w.parent = this; + } + } + this.setSize?.(this.computeSize()) + return r + + } + + const onRemoved = nodeType.prototype.onRemoved; + nodeType.prototype.onRemoved = function (message) { + const r = onRemoved ? onRemoved.apply(this, message) : undefined; + if (!this.widgets) return r + for (const w of this.widgets) { + if (w.canvas) { + w.canvas.remove(); + } + w.onRemove?.() + w.onRemoved?.() + } + return r + + } } + break } case "Animation Builder (mtb)": { From ee17d57c3d6d71fda1a5acc2cf85f936c525bc87 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 05:04:34 +0200 Subject: [PATCH 42/70] =?UTF-8?q?test:=20=F0=9F=94=A7=20pipe=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- install.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/install.py b/install.py index 7544842..25f07ca 100644 --- a/install.py +++ b/install.py @@ -11,6 +11,7 @@ from pathlib import Path import sys import zipfile import shutil +import stat here = Path(__file__).parent @@ -136,6 +137,19 @@ pip_map = { } +def is_pipe(): + try: + mode = os.fstat(0).st_mode + return ( + stat.S_ISFIFO(mode) + or stat.S_ISREG(mode) + or stat.S_ISBLK(mode) + or stat.S_ISSOCK(mode) + ) + except OSError: + return False + + # Get the version from __init__.py def get_local_version(): init_file = os.path.join(os.path.dirname(__file__), "__init__.py") @@ -248,6 +262,10 @@ def install_dependencies(dry=False): if __name__ == "__main__": + if is_pipe(): + print("Pipe detected, full install...") + sys.exit() + # Parse command-line arguments parser = argparse.ArgumentParser() parser.add_argument( From 3c66de2500a89efd2d2e3af88fc58429af725789 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 06:10:17 +0200 Subject: [PATCH 43/70] =?UTF-8?q?fix:=20=F0=9F=9A=80=20try=20to=20support?= =?UTF-8?q?=20remote=20install=20too?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- install.py | 43 ++++++++++++++++++++++++----------------- requirements-wheels.txt | 4 +++- requirements.txt | 2 -- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/install.py b/install.py index 25f07ca..3fb1947 100644 --- a/install.py +++ b/install.py @@ -15,7 +15,6 @@ import stat here = Path(__file__).parent -wheels_directory = here / "wheels" executable = sys.executable # - detect mode @@ -184,8 +183,8 @@ def download_file(url, file_name): progress_bar.update(len(chunk)) -def get_requirements(path): - with open(path, "r") as requirements_file: +def get_requirements(path: Path): + with open(path.resolve(), "r") as requirements_file: requirements_txt = requirements_file.read() try: @@ -218,6 +217,7 @@ def import_or_install(requirement, dry=False): color="green", ) except ImportError: + print_formatted(f"Installing package {pip_name}...", "italic", color="yellow") if dry: print_formatted( f"Dry-run: Package {pip_name} would be installed (import name: '{import_name}').", @@ -262,9 +262,20 @@ def install_dependencies(dry=False): if __name__ == "__main__": + full = False if is_pipe(): - print("Pipe detected, full install...") - sys.exit() + print_formatted("Pipe detected, full install...", color="green") + # we clone our repo + url = "https://github.com/melmass/comfy_mtb.git" + clone_dir = here / "custom_nodes" / "comfy_mtb" + if not clone_dir.exists(): + clone_dir.parent.mkdir(parents=True, exist_ok=True) + print_formatted(f"Cloning {url} to {clone_dir}", "italic", color="yellow") + subprocess.check_call(["git", "clone", "--recursive", url, clone_dir]) + + # os.chdir(clone_dir) + here = clone_dir + full = True # Parse command-line arguments parser = argparse.ArgumentParser() @@ -280,19 +291,21 @@ if __name__ == "__main__": help="Print what will happen without doing it (still making requests to the GH Api)", ) - parser.add_argument( - "--version", - default=get_local_version(), - help="Version to check against the GitHub API", - ) + # parser.add_argument( + # "--version", + # default=get_local_version(), + # help="Version to check against the GitHub API", + # ) args = parser.parse_args() + wheels_directory = here / "wheels" + print(f"Detected environment: {apply_color(mode,'cyan')}") # Install dependencies from requirements.txt # if args.requirements or mode == "venv": install_dependencies(dry=args.dry) - if not args.wheels and mode not in ["colab", "embeded"]: + if (not args.wheels and mode not in ["colab", "embeded"]) or not full: print_formatted( "Skipping wheel installation. Use --wheels to install wheel dependencies. (only needed for Comfy embed)", "italic", @@ -304,6 +317,7 @@ if __name__ == "__main__": print( f"Downloading and installing release wheels since we are in a Comfy {apply_color(mode,'cyan')} environment", ) + # Fetch the JSON data from the GitHub API URL owner = "melmass" repo = "comfy_mtb" @@ -353,13 +367,6 @@ if __name__ == "__main__": asset_download_url = asset["browser_download_url"] print_formatted(f"Downloading asset: {asset_name}", color="yellow") asset_dest = wheels_directory / asset_name - if asset_dest.exists(): - print_formatted( - f"Asset '{asset_dest}' already exists. Skipping.", - "italic", - color="yellow", - ) - continue download_file(asset_download_url, asset_dest) # - Unzip to wheels dir diff --git a/requirements-wheels.txt b/requirements-wheels.txt index 9a2f34a..91e7cb1 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -1,3 +1,5 @@ insightface==0.7.3 mmcv==2.0.0 -mmdet==3.0.0 \ No newline at end of file +mmdet==3.0.0 +facexlib==0.3.0 +basicsr==1.4.2 diff --git a/requirements.txt b/requirements.txt index ce1906f..3b936dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,6 @@ imageio===2.28.1 qrcode[pil] numpy==1.23.5 rembg==2.0.37 -facexlib==0.3.0 -basicsr==1.4.2 # on windows non WSL 2.10 is the last version with GPU support tensorflow<2.11.0; platform_system == "Windows" tb-nightly==2.12.0a20230126; platform_system == "Windows" From b40730ddbc3f8e3e7d5a17e9e9e4526ff37977fd Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 18:02:49 +0200 Subject: [PATCH 44/70] =?UTF-8?q?fix:=20=E2=9C=A8=20install=20process=20te?= =?UTF-8?q?sted=20in=20comfy-manager=20(embed,=20colab)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/package_wheels.yml | 4 +- install.py | 59 +++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/.github/workflows/package_wheels.yml b/.github/workflows/package_wheels.yml index d2a9cd1..4f7eba5 100644 --- a/.github/workflows/package_wheels.yml +++ b/.github/workflows/package_wheels.yml @@ -35,9 +35,7 @@ jobs: - name: 📦 Building and Bundling wheels shell: bash run: | - python -m pip wheel --no-cache-dir -r requirements-wheels.txt -w ./wheels > build.log - - cat build.log + python -m pip wheel --no-cache-dir --no-deps -r requirements-wheels.txt -w ./wheels 2>&1 | tee build.log # find source wheels packages=$(cat build.log | awk -F 'Building wheels for collected packages: ' '{print $2}') diff --git a/install.py b/install.py index 3fb1947..06aefcd 100644 --- a/install.py +++ b/install.py @@ -102,7 +102,11 @@ def print_formatted(text, *formats, color=None, background=None, **kwargs): formatted_text = apply_format(text, *formats) formatted_text = apply_color(formatted_text, color, background) file = kwargs.get("file", sys.stdout) - print(formatted_text, file=file) + print( + apply_color(apply_format("[mtb install] ", "bold"), color="yellow"), + formatted_text, + file=file, + ) # endregion @@ -201,9 +205,10 @@ def get_requirements(path: Path): return parsed_requirements -def import_or_install(requirement, dry=False): +def try_import(requirement): dependency = requirement.name.strip() import_name = pip_map.get(dependency, dependency) + installed = False pip_name = dependency if specs := requirement.specs: @@ -216,7 +221,17 @@ def import_or_install(requirement, dry=False): "bold", color="green", ) + installed = True except ImportError: + pass + + return (installed, pip_name, import_name) + + +def import_or_install(requirement, dry=False): + installed, pip_name, import_name = try_import(requirement) + + if not installed: print_formatted(f"Installing package {pip_name}...", "italic", color="yellow") if dry: print_formatted( @@ -277,6 +292,15 @@ if __name__ == "__main__": here = clone_dir full = True + if len(sys.argv) == 1: + print_formatted( + "No arguments provided, doing a full install/update...", + "italic", + color="yellow", + ) + + full = True + # Parse command-line arguments parser = argparse.ArgumentParser() parser.add_argument( @@ -298,14 +322,15 @@ if __name__ == "__main__": # ) args = parser.parse_args() + wheels_directory = here / "wheels" - print(f"Detected environment: {apply_color(mode,'cyan')}") + print_formatted(f"Detected environment: {apply_color(mode,'cyan')}") # Install dependencies from requirements.txt # if args.requirements or mode == "venv": install_dependencies(dry=args.dry) - if (not args.wheels and mode not in ["colab", "embeded"]) or not full: + if (not args.wheels and mode not in ["colab", "embeded"]) and not full: print_formatted( "Skipping wheel installation. Use --wheels to install wheel dependencies. (only needed for Comfy embed)", "italic", @@ -314,9 +339,29 @@ if __name__ == "__main__": sys.exit() if mode in ["colab", "embeded"]: - print( + print_formatted( f"Downloading and installing release wheels since we are in a Comfy {apply_color(mode,'cyan')} environment", ) + if full: + print_formatted( + f"Downloading and installing release wheels since no arguments where provided" + ) + + # - Check the env before proceeding. + missing_wheels = False + parsed_requirements = get_requirements(here / "requirements-wheels.txt") + if parsed_requirements: + for requirement in parsed_requirements: + installed, pip_name, import_name = try_import(requirement) + if not installed: + missing_wheels = True + break + + if not missing_wheels: + print_formatted( + f"All required wheels are already installed.", "italic", color="green" + ) + sys.exit() # Fetch the JSON data from the GitHub API URL owner = "melmass" @@ -358,7 +403,9 @@ if __name__ == "__main__": asset for asset in tag_data["assets"] if current_platform in asset["name"] ] if not matching_assets: - print(f"Unsupported operating system: {current_platform}") + print_formatted( + f"Unsupported operating system: {current_platform}", color="yellow" + ) wheels_directory.mkdir(exist_ok=True) From 0df55def29fb992751010f6b8a707699f230ff37 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 18:19:32 +0200 Subject: [PATCH 45/70] =?UTF-8?q?fix:=20=E2=9C=A8=20also=20try=20to=20copy?= =?UTF-8?q?=20web=20if=20symlink=20fails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 89fd22c..2423e2b 100644 --- a/__init__.py +++ b/__init__.py @@ -94,9 +94,28 @@ web_mtb = web_extensions_root / "mtb" if web_mtb.exists(): log.debug(f"Web extensions folder found at {web_mtb}") + if not os.path.islink(web_mtb.as_posix()): + log.warn( + f"Web extensions folder at {web_mtb} is not a symlink, if updating please delete it before" + ) + + elif web_extensions_root.exists(): + web_tgt = here / "web" try: - os.symlink((here / "web"), web_mtb.as_posix()) + os.symlink(web_tgt.as_posix(), web_mtb.as_posix()) + except OSError: + log.warn(f"Failed to create symlink to {web_mtb}, trying to copy it") + try: + import shutil + + shutil.copytree(web_tgt, web_mtb) + log.info(f"Successfully copied {web_tgt} to {web_mtb}") + except Exception: + log.warn( + f"Failed to symlink and copy {web_tgt} to {web_mtb}. Please copy the folder manually." + ) + except Exception: # OSError log.warn( f"Failed to create symlink to {web_mtb}. Please copy the folder manually." From 7be37dbbfac45e8038f94ced8a2fa8ec2b06fb34 Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 18:41:59 +0200 Subject: [PATCH 46/70] =?UTF-8?q?feat:=20=E2=9C=A8=20update=20install=20in?= =?UTF-8?q?structions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INSTALL-CN.md | 135 +++++++++++++++++++++++++++++--------------------- INSTALL-JP.md | 120 ++++++++++++++++++++++++++------------------ INSTALL.md | 64 ++++++++++++++---------- 3 files changed, 188 insertions(+), 131 deletions(-) diff --git a/INSTALL-CN.md b/INSTALL-CN.md index 479f46c..934e540 100644 --- a/INSTALL-CN.md +++ b/INSTALL-CN.md @@ -1,72 +1,93 @@ # 安装 - - [安装](#安装) - - [依赖项](#依赖项) - - [自定义虚拟环境(我主要使用此方法)](#自定义虚拟环境我主要使用此方法) - - [Comfy-portable / 独立安装(来自 ComfyUI 发布)](#comfy-portable--独立安装来自-comfyui-发布) - - [Google Colab](#google-colab) - - [模型下载](#模型下载) - - [Web 扩展](#web-扩展) + - [自动安装(推荐)](#自动安装推荐) + - [ComfyUI 管理器](#comfyui-管理器) + - [虚拟环境](#虚拟环境) + - [模型下载](#模型下载) + - [网络扩展](#网络扩展) + - [旧的安装方法 (MANUAL)](#旧的安装方法-manual) + - [依赖关系](#依赖关系) +### 自动安装(推荐) + +### ComfyUI 管理器 + +从 0.1.0 版开始,该扩展将使用 [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager) 进行安装,这对处理各种环境下的各种安装问题大有帮助。 + +### 虚拟环境 +还有一种试验性的单行安装方法,即在 ComfyUI 根目录下使用以下命令进行安装。它将下载代码、安装依赖项并运行安装脚本: -### 依赖项 - -#### 自定义虚拟环境(我主要使用此方法) - -1. 确保您处于用于 ComfyUI 的 Python 环境中。 -2. 运行以下命令安装所需的依赖项: - ```bash - pip install -r comfy_mtb/requirements.txt - ``` - -#### Comfy-portable / 独立安装(来自 ComfyUI 发布) - -如果您使用的是 ComfyUI 独立版的 `python-embeded`,则无法使用二进制文件进行依赖项的 pip 安装,特别是当它们没有预编译的 wheels 时。在这种情况下,请查看最新的[发布版](https://github.com/melMass/comfy_mtb/releases),其中包含了为 Linux 和 Windows 构建的预编译 wheels(仅对需要从源代码构建的部分)。有关更多信息,请参阅[此问题(#1)](https://github.com/melMass/comfy_mtb/issues/1)。 -![image](https://github.com/melMass/comfy_mtb/assets/7041726/2934fa14-3725-427c-8b9e-2b4f60ba1b7b) - - -#### Google Colab - -在 **运行 ComfyUI with localtunnel(推荐方式)** 标题下方添加一个新的代码单元格(在代码单元格之前)。 -![在 Colab 中添加位置的预览](https://github.com/melMass/comfy_mtb/assets/7041726/35df2ef1-14f9-44cd-aa65-353829188cd7) - - -```python -# 下载节点 -!git clone --recursive https://github.com/melMass/comfy_mtb.git custom_nodes/comfy_mtb - -# 下载所有模型 -!python custom_nodes/comfy_mtb/scripts/download_models.py -y - -# 安装依赖项 -!pip install -r custom_nodes/comfy_mtb/requirements.txt -f https://download.openmmlab.com/mmcv/dist/cu118/torch2.0/index.html +```bash +curl -sSL "https://raw.githubusercontent.com/username/repo/main/install.py" | python3 - ``` -如果运行后,Colab 抱怨需要重新启动运行时,请重新启动,并且不要重新运行之前的单元格,只需运行一个用于运行 localtunnel 的单元格即可(您可能需要先添加一个 `%cd ComfyUI` 的单元格...)。 -> **注意**: -> 如果您不需要所有模型,请将 `-y` 删除,因为 Colab 实际上支持用户输入: ![image](https://github.com/melMass/comfy_mtb/assets/7041726/40fc3602-f1d4-432a-98fd-ce2240f5ad06) +## 模型下载 +某些节点需要下载额外的模型,您可以使用与上述相同的 python 环境以交互方式完成下载: -> **预览** -> ![image](https://github.com/melMass/comfy_mtb/assets/7041726/b5b2b2d9-f1e8-4c43-b1db-7dfc5e07be86) - - - -### 模型下载 -某些节点需要下载额外的模型,您可以使用与上述相同的 Python 环境进行交互式下载: ```bash python scripts/download_models.py ``` -然后按照提示操作或只需按 Enter 键下载所有模型。 +然后根据提示或直接按回车键下载每个模型。 -> **注意** -> 您可以使用以下命令无需提示下载所有模型: -> ```bash -> python scripts/download_models.py -y -> ``` +> **Note** +> 您可以使用以下方法下载所有型号,无需提示: + ```bash + python scripts/download_models.py -y + ``` -### Web 扩展 +#### 网络扩展 -第一次运行脚本时,会[尝试创建符号链接](https://github.com/melMass/comfy_mtb/blob/d982b69a58c05ccead9c49370764beaa4549992a/__init__.py#L45-L61),将[Web 扩展](https://github.com/melMass/comfy_mtb/tree/main/web)链接到 Comfy 的 `web/extensions` 文件夹。如果失败,您可以手动将 `mtb` 文件夹复制到 `ComfyUI/web/extensions`,目前它只提供了一个颜色小部件,由几个节点共享: +首次运行时,脚本会尝试将 [网络扩展](https://github.com/melMass/comfy_mtb/tree/main/web)链接到你的 "web/extensions "文件夹,[请参阅](https://github.com/melMass/comfy_mtb/blob/d982b69a58c05ccead9c49370764beaa4549992a/__init__.py#L45-L61)。 + +color widget preview + +### 旧的安装方法 (MANUAL) +### 依赖关系 +

Custom Virtualenv(我主要用这个)

+ +

Comfy 便携式/单机版(来自 ComfyUI 版本)

+ +如果您使用 ComfyUI 单机版中的 `python-embeded `,那么当二进制文件没有轮子时,您就无法使用 pip 安装二进制文件的依赖项,在这种情况下,请查看最近的 [发布](https://github.com/melMass/comfy_mtb/releases),那里有一个预编译轮子的 linux 和 windows 捆绑包(只有那些需要从源代码编译的轮子),请查看 [此问题 (#1)](https://github.com/melMass/comfy_mtb/issues/1) 以获取更多信息。 +![image](https://github.com/melMass/comfy_mtb/assets/7041726/2934fa14-3725-427c-8b9e-2b4f60ba1b7b) + + +
+ +

Google Colab

+ +在 **Run ComfyUI with localtunnel (Recommended Way)** 标题之后(代码单元格之前)添加一个新的代码单元格 + +![preview of where to add it on colab](https://github.com/melMass/comfy_mtb/assets/7041726/35df2ef1-14f9-44cd-aa65-353829188cd7) + + +```python +# download the nodes +!git clone --recursive https://github.com/melMass/comfy_mtb.git custom_nodes/comfy_mtb + +# download all models +!python custom_nodes/comfy_mtb/scripts/download_models.py -y + +# install the dependencies +!pip install -r custom_nodes/comfy_mtb/requirements.txt -f https://download.openmmlab.com/mmcv/dist/cu118/torch2.0/index.html +``` + +如果运行后 colab 抱怨需要重新启动运行时,请重新启动,然后不要重新运行之前的单元格,只运行运行本地隧道的单元格。(可能需要先添加一个包含 `%cd ComfyUI` 的单元格) + + +> **Note**: +> If you don't need all models, remove the `-y` as collab actually supports user input: ![image](https://github.com/melMass/comfy_mtb/assets/7041726/40fc3602-f1d4-432a-98fd-ce2240f5ad06) + +> **Preview** +> ![image](https://github.com/melMass/comfy_mtb/assets/7041726/b5b2b2d9-f1e8-4c43-b1db-7dfc5e07be86) + +
-color widget preview \ No newline at end of file diff --git a/INSTALL-JP.md b/INSTALL-JP.md index 61212ee..f06dc6f 100644 --- a/INSTALL-JP.md +++ b/INSTALL-JP.md @@ -1,71 +1,93 @@ # インストール + - [インストール](#インストール) + - [自動インストール (推奨)](#自動インストール-推奨) + - [ComfyUI マネージャ](#comfyui-マネージャ) + - [仮想環境](#仮想環境) + - [モデルのダウンロード](#モデルのダウンロード) + - [ウェブ拡張機能](#ウェブ拡張機能) + - [旧インストール方法 (MANUAL)](#旧インストール方法-manual) - [依存関係](#依存関係) - - [カスタム仮想環境(主に使用)](#カスタム仮想環境主に使用) - - [Comfyポータブル/スタンドアロン(ComfyUIリリースから)](#comfyポータブルスタンドアロンcomfyuiリリースから) - - [Google Colab](#google-colab) - - [モデルのダウンロード](#モデルのダウンロード) - - [Web拡張機能](#web拡張機能) -### 依存関係 +## 自動インストール (推奨) -#### カスタム仮想環境(主に使用) +### ComfyUI マネージャ -1. ComfyUIで使用するPython環境にいることを確認してください。 -2. 次のコマンドを実行して、必要な依存関係をインストールします。 - ```bash - pip install -r comfy_mtb/requirements.txt - ``` -#### Comfyポータブル/スタンドアロン(ComfyUIリリースから) +バージョン0.1.0では、この拡張機能は[ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager)と一緒にインストールすることを想定しています。これは、様々な環境で直面する様々なインストール問題を処理するのに非常に役立ちます。 -ComfyUIスタンドアロンの`python-embeded`を使用している場合、ホイールがない場合にはバイナリの依存関係をpipでインストールすることはできません。この場合は、最新の[リリース](https://github.com/melMass/comfy_mtb/releases)を確認してください。ビルド元のソースが必要な場合は、linuxとwindows用のプリビルドホイールがバンドルされています。詳細については、[この問題(#1)](https://github.com/melMass/comfy_mtb/issues/1)を参照してください。 -![image](https://github.com/melMass/comfy_mtb/assets/7041726/2934fa14-3725-427c-8b9e-2b4f60ba1b7b) +### 仮想環境 +また、ComfyUIのルートから以下のコマンドを使用する実験的なワンライナー・インストールもあります。これはコードをダウンロードし、依存関係をインストールし、インストールスクリプトを実行します: - -#### Google Colab - -**Run ComfyUI with localtunnel (Recommended Way)**ヘッダーのすぐ後ろに新しいコードセルを追加します(コードセルの前) -![colabでの追加方法のプレビュー](https://github.com/melMass/comfy_mtb/assets/7041726/35df2ef1-14f9-44cd-aa65-353829188cd7) - - -```python -# ノードをダウンロード -!git clone --recursive https://github.com/melMass/comfy_mtb.git custom_nodes/comfy_mtb - -# すべてのモデルをダウンロード -!python custom_nodes/comfy_mtb/scripts/download_models.py -y - -# 依存関係をインストール -!pip install -r custom_nodes/comfy_mtb/requirements.txt -f https://download.openmmlab.com/mmcv/dist/cu118/torch2.0/index.html +```bash +curl -sSL "https://raw.githubusercontent.com/username/repo/main/install.py" | python3 - ``` -これを実行した後、colabがランタイムを再起動するようにクレームをつける場合は、再起動してから以前のセルを再実行せずに、ローカルトンネルを実行するセルのみを実行します(最初に`%cd ComfyUI`というセルを追加する必要があるかもしれません...) - -> **注意**: -> すべてのモデルが必要ない場合は、collabは実際にユーザー入力をサポートしているため、`-y`を削除します: ![image](https://github.com/melMass/comfy_mtb/assets/7041726/40fc3602-f1d4-432a-98fd-ce2240f5ad06) - -> **プレビュー** -> ![image](https://github.com/melMass/comfy_mtb/assets/7041726/b5b2b2d9-f1e8-4c43-b1db-7dfc5e07be86) - - - -### モデルのダウンロード -一部のノードには追加のモデルのダウンロードが必要です。上記と同じPython環境を使用して対話的に行うことができます。 +## モデルのダウンロード +ノードによっては、追加モデルのダウンロードが必要な場合があるので、上記と同じ python 環境を使って対話的に行うことができる: ```bash python scripts/download_models.py ``` -その後、プロンプトに従ってモデルをダウンロードするか、単にEnterキーを押してすべてのモデルをダウンロードします。 +プロンプトに従うか、Enterを押すだけで全てのモデルをダウンロードできます。 -> **注意** -> プロンプトなしですべてのモデルをダウンロードするには、次のコマンドを使用します: + +> **Note** +> プロンプトを出さずに全てのモデルをダウンロードするには、以下のようにします: ```bash python scripts/download_models.py -y ``` -### Web拡張機能 +### ウェブ拡張機能 -最初の実行時に、スクリプトは[web拡張機能](https://github.com/melMass/comfy_mtb/tree/main/web)をComfyの`web/extensions`フォルダにシンボリックリンクしようとします。これが失敗する場合は、`mtb`フォルダを手動で`ComfyUI/web/extensions`にコピーしてください。現時点では、いくつかのノードで共有されるカラーウィジェットのみが提供されています。 +初回実行時にスクリプトは[web extensions](https://github.com/melMass/comfy_mtb/tree/main/web)をあなたの快適な `web/extensions` フォルダに[シンボリックリンク](https://github.com/melMass/comfy_mtb/blob/d982b69a58c05ccead9c49370764beaa4549992a/__init__.py#L45-L61)しようとします。万が一失敗した場合は、mtbフォルダを手動で`ComfyUI/web/extensions`にコピーしてください: + +color widget preview + +## 旧インストール方法 (MANUAL) +### 依存関係 + +

カスタム Virtualenv (私は主にこれを使っています)

+ +1. ComfyUIで使用しているPython環境であることを確認してください。 +2. 以下のコマンドを実行して、必要な依存関係をインストールします: + ```bash + pip install -r comfy_mtb/requirements.txt + ``` + +
+ +

Comfy-portable / standalone (ComfyUI リリースより)

。 + +もしあなたがComfyUIスタンドアロンから`python-embeded`を使用している場合、バイナリがホイールを持っていない場合、依存関係をpipでインストールすることができません。この場合、最後の[リリース](https://github.com/melMass/comfy_mtb/releases)をチェックしてください。(ソースからのビルドが必要なもののみ)あらかじめビルドされたホイールがあるlinuxとwindows用のバンドルがあります。詳細は[この問題(#1)](https://github.com/melMass/comfy_mtb/issues/1)をチェックしてください。 + +![image](https://github.com/melMass/comfy_mtb/assets/7041726/2934fa14-3725-427c-8b9e-2b4f60ba1b7b) + +
+ +

Google Colab

+ +ComfyUI with localtunnel (Recommended Way)**ヘッダーのすぐ後(コードセルの前)に、新しいコードセルを追加してください。 +![colabに追加する場所のプレビュー](https://github.com/melMass/comfy_mtb/assets/7041726/35df2ef1-14f9-44cd-aa65-353829188cd7) + +```python +# download the nodes +!git clone --recursive https://github.com/melMass/comfy_mtb.git custom_nodes/comfy_mtb + +# download all models +!python custom_nodes/comfy_mtb/scripts/download_models.py -y + +# install the dependencies +!pip install -r custom_nodes/comfy_mtb/requirements.txt -f https://download.openmmlab.com/mmcv/dist/cu118/torch2.0/index.html +``` +これを実行した後、colabがランタイムを再起動する必要があると文句を言ったら、それを実行し、それ以前のセルは再実行せず、localtunnelを実行するセルだけを再実行してください。(最初に`%cd ComfyUI`のセルを追加する必要があるかもしれません...) + + +> **Note**: +> すべてのモデルが必要でない場合は、`-y`を削除してください : ![image](https://github.com/melMass/comfy_mtb/assets/7041726/40fc3602-f1d4-432a-98fd-ce2240f5ad06) + +> **プレビュー** +> ![image](https://github.com/melMass/comfy_mtb/assets/7041726/b5b2b2d9-f1e8-4c43-b1db-7dfc5e07be86) + +
-color widget preview \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md index 3ccd5e0..2bf392d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,13 +1,47 @@ # Installation - [Installation](#installation) - - [Dependencies](#dependencies) - - [Custom virtualenv (I use this mainly)](#custom-virtualenv-i-use-this-mainly) - - [Comfy-portable / standalone (from ComfyUI releases)](#comfy-portable--standalone-from-comfyui-releases) - - [Google Colab](#google-colab) - - [Models Download](#models-download) + - [Automatic Install (Recommended)](#automatic-install-recommended) + - [ComfyUI Manager](#comfyui-manager) + - [Virtual Env](#virtual-env) + - [Models Download](#models-download) - [Web Extensions](#web-extensions) + - [Old installation method (MANUAL)](#old-installation-method-manual) + - [Dependencies](#dependencies) +## Automatic Install (Recommended) +### ComfyUI Manager + +As of version 0.1.0, this extension is meant to be installed with the [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager), which helps a lot with handling the various install issues faced by various environments. + +### Virtual Env +There is also an experimental one liner install using the following command from ComfyUI's root. It will download the code, install the dependencies and run the install script: + +```bash +curl -sSL "https://raw.githubusercontent.com/username/repo/main/install.py" | python3 - +``` + +## Models Download +Some nodes require extra models to be downloaded, you can interactively do it using the same python environment as above: +```bash +python scripts/download_models.py +``` + +then follow the prompt or just press enter to download every models. + +> **Note** +> You can use the following to download all models without prompt: + ```bash + python scripts/download_models.py -y + ``` + +### Web Extensions + +On first run the script [tries to symlink](https://github.com/melMass/comfy_mtb/blob/d982b69a58c05ccead9c49370764beaa4549992a/__init__.py#L45-L61) the [web extensions](https://github.com/melMass/comfy_mtb/tree/main/web) to your comfy `web/extensions` folder. In case it fails you can manually copy the mtb folder to `ComfyUI/web/extensions` it only provides a color widget for now shared by a few nodes: + +color widget preview + +## Old installation method (MANUAL) ### Dependencies

Custom Virtualenv (I use this mainly)

@@ -55,23 +89,3 @@ If after running this, colab complains about needing to restart runtime, do it,
- -### Models Download -Some nodes require extra models to be downloaded, you can interactively do it using the same python environment as above: -```bash -python scripts/download_models.py -``` - -then follow the prompt or just press enter to download every models. - -> **Note** -> You can use the following to download all models without prompt: - ```bash - python scripts/download_models.py -y - ``` - -### Web Extensions - -On first run the script [tries to symlink](https://github.com/melMass/comfy_mtb/blob/d982b69a58c05ccead9c49370764beaa4549992a/__init__.py#L45-L61) the [web extensions](https://github.com/melMass/comfy_mtb/tree/main/web) to your comfy `web/extensions` folder. In case it fails you can manually copy the mtb folder to `ComfyUI/web/extensions` it only provides a color widget for now shared by a few nodes: - -color widget preview From 119b4d6e16c2a90db1664ccaac748507feb73ea0 Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Sat, 22 Jul 2023 19:07:56 +0200 Subject: [PATCH 47/70] =?UTF-8?q?fix:=20=F0=9F=94=A5=20properly=20match=20?= =?UTF-8?q?built=20wheels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-wheels.txt | 2 -- requirements.txt | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-wheels.txt b/requirements-wheels.txt index 91e7cb1..af6449d 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -1,5 +1,3 @@ insightface==0.7.3 mmcv==2.0.0 -mmdet==3.0.0 -facexlib==0.3.0 basicsr==1.4.2 diff --git a/requirements.txt b/requirements.txt index 3b936dd..462486e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,6 @@ tb-nightly==2.12.0a20230126; platform_system == "Windows" tensorflow; platform_system != "Windows" # the old tf version on windows comes with a breaking protobuf version protobuf==3.19.6 -gdown @ git+https://github.com/melMass/gdown@main \ No newline at end of file +gdown @ git+https://github.com/melMass/gdown@main +mmdet==3.0.0 +facexlib==0.3.0 From a393793cfa93721eac46295723076a1dda940dcd Mon Sep 17 00:00:00 2001 From: melMass Date: Sat, 22 Jul 2023 20:09:31 +0200 Subject: [PATCH 48/70] =?UTF-8?q?fix:=20=F0=9F=94=A5=20use=20BOOL=20everyw?= =?UTF-8?q?here?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/deep_bump.py | 8 +++----- nodes/faceenhance.py | 16 ++++++---------- nodes/faceswap.py | 5 +++-- nodes/fun.py | 6 +++--- nodes/image_processing.py | 17 +++++++---------- nodes/mask.py | 12 ++++++------ 6 files changed, 28 insertions(+), 36 deletions(-) diff --git a/nodes/deep_bump.py b/nodes/deep_bump.py index 639aaa9..8be4836 100644 --- a/nodes/deep_bump.py +++ b/nodes/deep_bump.py @@ -264,7 +264,7 @@ class DeepBump: "LARGEST", ], ), - "normals_to_height_seamless": (["TRUE", "FALSE"],), + "normals_to_height_seamless": ("BOOL", {"default": False}), }, } @@ -279,7 +279,7 @@ class DeepBump: mode="Color to Normals", color_to_normals_overlap="SMALL", normals_to_curvature_blur_radius="SMALL", - normals_to_height_seamless="TRUE", + normals_to_height_seamless=True, ): image = utils_inference.tensor2pil(image) @@ -295,9 +295,7 @@ class DeepBump: in_img, normals_to_curvature_blur_radius, None ) if mode == "Normals to Height": - out_img = normals_to_height( - in_img, normals_to_height_seamless == "TRUE", None - ) + out_img = normals_to_height(in_img, normals_to_height_seamless, None) out_img = (np.transpose(out_img, (1, 2, 0)) * 255).astype(np.uint8) diff --git a/nodes/faceenhance.py b/nodes/faceenhance.py index e2317af..45d4fde 100644 --- a/nodes/faceenhance.py +++ b/nodes/faceenhance.py @@ -136,12 +136,12 @@ class RestoreFace: "image": ("IMAGE",), "model": ("FACEENHANCE_MODEL",), # Input are aligned faces - "aligned": (["true", "false"], {"default": "false"}), + "aligned": ("BOOL", {"default": False}), # Only restore the center face - "only_center_face": (["true", "false"], {"default": "false"}), + "only_center_face": ("BOOL", {"default": False}), # Adjustable weights "weight": ("FLOAT", {"default": 0.5}), - "save_tmp_steps": (["true", "false"], {"default": "true"}), + "save_tmp_steps": ("BOOL", {"default": True}), } } @@ -183,15 +183,11 @@ class RestoreFace: self, image: torch.Tensor, model: GFPGANer, - aligned="false", - only_center_face="false", + aligned=False, + only_center_face=False, weight=0.5, - save_tmp_steps="true", + save_tmp_steps=True, ) -> Tuple[torch.Tensor]: - save_tmp_steps = save_tmp_steps == "true" - aligned = aligned == "true" - only_center_face = only_center_face == "true" - out = [ self.do_restore( image[i], model, aligned, only_center_face, weight, save_tmp_steps diff --git a/nodes/faceswap.py b/nodes/faceswap.py index aa1fc25..c7d8f70 100644 --- a/nodes/faceswap.py +++ b/nodes/faceswap.py @@ -82,8 +82,9 @@ class FaceSwap: "reference": ("IMAGE",), "faces_index": ("STRING", {"default": "0"}), "faceswap_model": ("FACESWAP_MODEL", {"default": "None"}), + "debug": ("BOOL", {"default": False}), }, - "optional": {"debug": (["true", "false"], {"default": "false"})}, + "optional": {}, } RETURN_TYPES = ("IMAGE",) @@ -96,7 +97,7 @@ class FaceSwap: reference: torch.Tensor, faces_index: str, faceswap_model, - debug="false", + debug=False, ): def do_swap(img): model_management.throw_exception_if_processing_interrupted() diff --git a/nodes/fun.py b/nodes/fun.py index 5661686..d63cce2 100644 --- a/nodes/fun.py +++ b/nodes/fun.py @@ -72,7 +72,7 @@ class QrCode: "error_correct": (("L", "M", "Q", "H"), {"default": "L"}), "box_size": ("INT", {"default": 10, "max": 8096, "min": 0, "step": 1}), "border": ("INT", {"default": 4, "max": 8096, "min": 0, "step": 1}), - "invert": (("True", "False"), {"default": "False"}), + "invert": (("BOOL",), {"default": False}), } } @@ -99,8 +99,8 @@ class QrCode: qr.add_data(url) qr.make(fit=True) - back_color = (255, 255, 255) if invert == "True" else (0, 0, 0) - fill_color = (0, 0, 0) if invert == "True" else (255, 255, 255) + back_color = (255, 255, 255) if invert else (0, 0, 0) + fill_color = (0, 0, 0) if invert else (255, 255, 255) code = img = qr.make_image(back_color=back_color, fill_color=fill_color) diff --git a/nodes/image_processing.py b/nodes/image_processing.py index ad302be..acbb3c0 100644 --- a/nodes/image_processing.py +++ b/nodes/image_processing.py @@ -392,7 +392,7 @@ class ImagePremultiply: "required": { "image": ("IMAGE",), "mask": ("MASK",), - "invert": (["True", "False"], {"default": "False"}), + "invert": ("BOOL", {"default": False}), } } @@ -401,8 +401,6 @@ class ImagePremultiply: FUNCTION = "premultiply" def premultiply(self, image, mask, invert): - invert = invert == "True" - images = tensor2pil(image) if invert: masks = tensor2pil(mask) # .convert("L") @@ -445,7 +443,7 @@ class ImageResizeFactor: "FLOAT", {"default": 2, "min": 0.01, "max": 16.0, "step": 0.01}, ), - "supersample": (["true", "false"], {"default": "true"}), + "supersample": ("BOOL", {"default": True}), "resampling": ( ["lanczos", "nearest", "bilinear", "bicubic"], {"default": "lanczos"}, @@ -510,7 +508,7 @@ class ImageResizeFactor: resample_filters = {"nearest": 0, "bilinear": 2, "bicubic": 3, "lanczos": 1} # Apply supersample - if supersample == "true": + if supersample: super_size = (new_width * 8, new_height * 8) log.debug(f"Applying supersample: {super_size}") img = img.resize( @@ -529,12 +527,12 @@ class ImageResizeFactor: self, image: torch.Tensor, factor: float, - supersample: str, + supersample: bool, resampling: str, mask=None, ): log.debug(f"Resizing image with factor {factor} and resampling {resampling}") - supersample = supersample == "true" + batch_count = image.size(0) log.debug(f"Batch count: {batch_count}") if batch_count == 1: @@ -566,7 +564,7 @@ class SaveImageGrid: "required": { "images": ("IMAGE",), "filename_prefix": ("STRING", {"default": "ComfyUI"}), - "save_intermediate": (["true", "false"], {"default": "false"}), + "save_intermediate": ("BOOL", {"default": False}), }, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } @@ -607,11 +605,10 @@ class SaveImageGrid: self, images, filename_prefix="Grid", - save_intermediate="false", + save_intermediate=False, prompt=None, extra_pnginfo=None, ): - save_intermediate = save_intermediate == "true" ( full_output_folder, filename, diff --git a/nodes/mask.py b/nodes/mask.py index 6745dfe..56af63f 100644 --- a/nodes/mask.py +++ b/nodes/mask.py @@ -16,8 +16,8 @@ class ImageRemoveBackgroundRembg: "required": { "image": ("IMAGE",), "alpha_matting": ( - ["True", "False"], - {"default": "False"}, + "BOOL", + {"default": False}, ), "alpha_matting_foreground_threshold": ( "INT", @@ -32,8 +32,8 @@ class ImageRemoveBackgroundRembg: {"default": 10, "min": 0, "max": 255}, ), "post_process_mask": ( - ["True", "False"], - {"default": "False"}, + "BOOL", + {"default": False}, ), "bgcolor": ( "COLOR", @@ -76,13 +76,13 @@ class ImageRemoveBackgroundRembg: for img in images: img_rm = remove( data=img, - alpha_matting=alpha_matting == "True", + alpha_matting=alpha_matting, alpha_matting_foreground_threshold=alpha_matting_foreground_threshold, alpha_matting_background_threshold=alpha_matting_background_threshold, alpha_matting_erode_size=alpha_matting_erode_size, session=None, only_mask=False, - post_process_mask=post_process_mask == "True", + post_process_mask=post_process_mask, bgcolor=None, ) From 11128ff85a7e0b4a54f405548969c2478da26df6 Mon Sep 17 00:00:00 2001 From: melMass Date: Sun, 23 Jul 2023 01:19:19 +0200 Subject: [PATCH 49/70] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20TransformImage?= =?UTF-8?q?=20node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/image_interpolation.py | 4 +++ nodes/transform.py | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 nodes/transform.py diff --git a/nodes/image_interpolation.py b/nodes/image_interpolation.py index aa91a32..94d3ac8 100644 --- a/nodes/image_interpolation.py +++ b/nodes/image_interpolation.py @@ -43,6 +43,7 @@ class GetBatchFromHistory: "count": ("INT", {"default": 1, "min": 0}), "offset": ("INT", {"default": 0, "min": -1e9, "max": 1e9}), }, + "optional": {"passthrough_image": ("IMAGE",)}, } RETURN_TYPES = ("IMAGE",) @@ -55,8 +56,11 @@ class GetBatchFromHistory: enable=True, count=0, offset=0, + passthrough_image=None, ): if not enable or count == 0: + if passthrough_image is not None: + return (passthrough_image,) log.debug("Load from history is disabled for this iteration") return (torch.zeros(0),) frames = [] diff --git a/nodes/transform.py b/nodes/transform.py new file mode 100644 index 0000000..71d6580 --- /dev/null +++ b/nodes/transform.py @@ -0,0 +1,54 @@ +import torch +import torchvision.transforms.functional as F + + +class TransformImage: + """Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy + + + it return a tensor representing the transformed images with the same shape as the input tensor + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "x": ("FLOAT", {"default": 0}), + "y": ("FLOAT", {"default": 0}), + "zoom": ("FLOAT", {"default": 1.0, "min": 0.001}), + "angle": ("FLOAT", {"default": 0}), + "shear": ("FLOAT", {"default": 0}), + }, + } + + FUNCTION = "transform" + RETURN_TYPES = ("IMAGE",) + CATEGORY = "mtb/transform" + + def transform( + self, + image: torch.Tensor, + x: float, + y: float, + zoom: float, + angle: int, + shear, + ): + if image.size(0) == 0: + return (torch.zeros(0),) + transformed_images = [] + for img in image: + img = img.transpose(0, 2) + + transformed_image = F.affine( + img, angle=angle, scale=zoom, translate=[int(y), int(x)], shear=shear + ) + + transformed_image = transformed_image.transpose(2, 0) + transformed_images.append(transformed_image.unsqueeze(0)) + + return (torch.cat(transformed_images, dim=0),) + + +__nodes__ = [TransformImage] From 7142b284adc7fba9a1bdafd1a52621bfc168bde1 Mon Sep 17 00:00:00 2001 From: melMass Date: Sun, 23 Jul 2023 01:20:06 +0200 Subject: [PATCH 50/70] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20back=20Save=20T?= =?UTF-8?q?ensors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/debug.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/nodes/debug.py b/nodes/debug.py index ba6bdb9..2ca0ff9 100644 --- a/nodes/debug.py +++ b/nodes/debug.py @@ -2,6 +2,9 @@ from ..utils import tensor2pil from ..log import log import io, base64 import torch +import folder_paths +from typing import Optional +from pathlib import Path class Debug: @@ -54,4 +57,65 @@ class Debug: return output -__nodes__ = [Debug] +class SaveTensors: + """Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy""" + + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "mtb/debug" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "filename_prefix": ("STRING", {"default": "ComfyPickle"}), + }, + "optional": { + "image": ("IMAGE",), + "mask": ("MASK",), + "latent": ("LATENT",), + }, + } + + FUNCTION = "save" + OUTPUT_NODE = True + RETURN_TYPES = () + CATEGORY = "mtb/debug" + + def save( + self, + filename_prefix, + image: Optional[torch.Tensor] = None, + mask: Optional[torch.Tensor] = None, + latent: Optional[torch.Tensor] = None, + ): + ( + full_output_folder, + filename, + counter, + subfolder, + filename_prefix, + ) = folder_paths.get_save_image_path(filename_prefix, self.output_dir) + full_output_folder = Path(full_output_folder) + if image is not None: + image_file = f"{filename}_image_{counter:05}.pt" + torch.save(image, full_output_folder / image_file) + # np.save(full_output_folder/ image_file, image.cpu().numpy()) + + if mask is not None: + mask_file = f"{filename}_mask_{counter:05}.pt" + torch.save(mask, full_output_folder / mask_file) + # np.save(full_output_folder/ mask_file, mask.cpu().numpy()) + + if latent is not None: + # for latent we must use pickle + latent_file = f"{filename}_latent_{counter:05}.pt" + torch.save(latent, full_output_folder / latent_file) + # pickle.dump(latent, open(full_output_folder/ latent_file, "wb")) + + # np.save(full_output_folder/ latent_file, latent[""].cpu().numpy()) + + return f"{filename_prefix}_{counter:05}" + + +__nodes__ = [Debug, SaveTensors] From 10d05031b1791ab3534cf838be6eb75df638dfb6 Mon Sep 17 00:00:00 2001 From: melMass Date: Sun, 23 Jul 2023 01:21:20 +0200 Subject: [PATCH 51/70] =?UTF-8?q?docs:=20=F0=9F=93=9D=20add=20comfyforum?= =?UTF-8?q?=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shows a lot of the new nodes but require ComfyUI-Workflow-Component --- examples/advanced-comfyforum.json | 932 ++++++++++++++++++++++++++++++ 1 file changed, 932 insertions(+) create mode 100644 examples/advanced-comfyforum.json diff --git a/examples/advanced-comfyforum.json b/examples/advanced-comfyforum.json new file mode 100644 index 0000000..adc807f --- /dev/null +++ b/examples/advanced-comfyforum.json @@ -0,0 +1,932 @@ +{ + "last_node_id": 24, + "last_link_id": 30, + "nodes": [ + { + "id": 11, + "type": "Get Batch From History (mtb)", + "pos": [ + -828, + 522 + ], + "size": [ + 235.1999969482422, + 118 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "passthrough_image", + "type": "IMAGE", + "link": 10 + }, + { + "name": "enable", + "type": "BOOL", + "link": 9, + "widget": { + "name": "enable", + "config": [ + "BOOL", + { + "default": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "i", + "type": "IMAGE", + "links": [ + 26 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Get Batch From History (mtb)" + }, + "widgets_values": [ + false, + 1, + 0 + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 24, + "type": "Note", + "pos": [ + -827, + 406 + ], + "size": [ + 229.35427033319013, + 69.73426380916959 + ], + "flags": {}, + "order": 5, + "mode": 0, + "properties": { + "text": "" + }, + "widgets_values": [ + "On first frame we get the init image, on all subsequent ones the feedback from the previous queue item" + ], + "color": "#223", + "bgcolor": "#335", + "shape": 1 + }, + { + "id": 10, + "type": "LoadImage", + "pos": [ + -1388, + 321 + ], + "size": [ + 315, + 314 + ], + "flags": {}, + "order": 4, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 10 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "example.png", + "image" + ] + }, + { + "id": 13, + "type": "PrimitiveNode", + "pos": [ + -1679, + 817 + ], + "size": [ + 210, + 82 + ], + "flags": {}, + "order": 3, + "mode": 0, + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 8 + ], + "widget": { + "name": "int", + "config": [ + "INT", + { + "default": 0 + } + ] + } + } + ], + "title": "int", + "properties": {}, + "widgets_values": [ + 0, + "increment" + ] + }, + { + "id": 12, + "type": "Int To Bool (mtb)", + "pos": [ + -1290, + 818 + ], + "size": [ + 210, + 42.2801628112793 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "int", + "type": "INT", + "link": 8, + "widget": { + "name": "int", + "config": [ + "INT", + { + "default": 0 + } + ] + }, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "BOOL", + "type": "BOOL", + "links": [ + 9 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Int To Bool (mtb)" + }, + "widgets_values": [ + 0 + ] + }, + { + "id": 14, + "type": "Transform Image (mtb)", + "pos": [ + -54, + 515 + ], + "size": [ + 315, + 154 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 26 + }, + { + "name": "angle", + "type": "FLOAT", + "link": 30, + "widget": { + "name": "angle", + "config": [ + "FLOAT", + { + "default": 0 + } + ] + } + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 24 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Transform Image (mtb)" + }, + "widgets_values": [ + 0, + 5, + 1.02, + 0, + 0 + ] + }, + { + "id": 20, + "type": "Export To Prores (mtb)", + "pos": [ + 394, + 1803 + ], + "size": [ + 315, + 82 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 17 + } + ], + "outputs": [ + { + "name": "VIDEO", + "type": "VIDEO", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "Export To Prores (mtb)" + }, + "widgets_values": [ + 12, + "export" + ] + }, + { + "id": 19, + "type": "Save Gif (mtb)", + "pos": [ + 392, + 1321 + ], + "size": [ + 210, + 336 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 18 + } + ], + "properties": { + "Node name for S&R": "Save Gif (mtb)" + }, + "widgets_values": [ + 12, + 1, + true, + "/view?filename=8b62123f3c.gif&subfolder=&type=output" + ] + }, + { + "id": 16, + "type": "Styles Loader (mtb)", + "pos": [ + 72, + 213 + ], + "size": [ + 315, + 78 + ], + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "positive", + "type": "STRING", + "links": null, + "shape": 3 + }, + { + "name": "negative", + "type": "STRING", + "links": [ + 15 + ], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "Styles Loader (mtb)" + }, + "widgets_values": [ + "❌Low Token" + ] + }, + { + "id": 9, + "type": "CheckpointLoaderSimple", + "pos": [ + 64, + 68 + ], + "size": [ + 315, + 98 + ], + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 4 + ], + "shape": 3 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 5 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 6 + ], + "shape": 3, + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "revAnimated_v122.safetensors" + ] + }, + { + "id": 22, + "type": "Fit Number (mtb)", + "pos": [ + 39, + 842 + ], + "size": [ + 210, + 166 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "value", + "type": "FLOAT", + "link": 27 + } + ], + "outputs": [ + { + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 28 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "Fit Number (mtb)" + }, + "widgets_values": [ + true, + 0, + 1, + 0.9, + 0.2 + ] + }, + { + "id": 23, + "type": "Fit Number (mtb)", + "pos": [ + -342, + 621 + ], + "size": [ + 210, + 166 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "value", + "type": "FLOAT", + "link": 29 + } + ], + "outputs": [ + { + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 30 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Fit Number (mtb)" + }, + "widgets_values": [ + true, + 0.7, + 1, + 0, + 3 + ] + }, + { + "id": 8, + "type": "## i2i [cac944]", + "pos": [ + 567, + 65 + ], + "size": [ + 400, + 230 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 4, + "slot_index": 0 + }, + { + "name": "clip", + "type": "CLIP", + "link": 5 + }, + { + "name": "vae", + "type": "VAE", + "link": 6 + }, + { + "name": "image", + "type": "IMAGE", + "link": 24 + }, + { + "name": "negative", + "type": "STRING", + "link": 15, + "widget": { + "name": "negative", + "config": [ + "STRING", + { + "multiline": true, + "placeholder": "negative", + "default": "" + } + ] + }, + "slot_index": 4 + }, + { + "name": "denoise", + "type": "FLOAT", + "link": 28, + "widget": { + "name": "denoise", + "config": [ + "FLOAT", + { + "default": 0.6, + "min": 0, + "max": 1, + "step": 0.01 + } + ] + }, + "slot_index": 5 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 25 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "LATENT", + "type": "LATENT", + "links": null, + "shape": 3 + } + ], + "title": "## i2i", + "properties": { + "Node name for S&R": "## i2i [cac944]" + }, + "widgets_values": [ + "A blond girl in a pink dress, clear sky, 1958", + "", + 0.2599999999999997 + ] + }, + { + "id": 15, + "type": "SaveImage", + "pos": [ + 1054, + 67 + ], + "size": [ + 1110.1168110198978, + 630.0240418701173 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 25 + } + ], + "properties": {}, + "widgets_values": [ + "ComfyUI" + ] + }, + { + "id": 17, + "type": "Animation Builder (mtb)", + "pos": [ + -1032, + 1274 + ], + "size": [ + 210, + 306 + ], + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "frame", + "type": "INT", + "links": null, + "shape": 3 + }, + { + "name": "0-1 (scaled)", + "type": "FLOAT", + "links": [ + 27, + 29 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "count", + "type": "INT", + "links": null, + "shape": 3 + }, + { + "name": "loop_ended", + "type": "BOOL", + "links": [ + 16 + ], + "shape": 3, + "slot_index": 3 + } + ], + "properties": { + "Node name for S&R": "Animation Builder (mtb)" + }, + "widgets_values": [ + 30, + 1, + 1, + 90, + 1, + "raw: 90\nframe: 0", + "Done 😎!", + "reset", + "queue" + ] + }, + { + "id": 18, + "type": "Get Batch From History (mtb)", + "pos": [ + -369, + 1511 + ], + "size": [ + 235.1999969482422, + 118 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "passthrough_image", + "type": "IMAGE", + "link": null + }, + { + "name": "enable", + "type": "BOOL", + "link": 16, + "widget": { + "name": "enable", + "config": [ + "BOOL", + { + "default": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "i", + "type": "IMAGE", + "links": [ + 17, + 18 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Get Batch From History (mtb)" + }, + "widgets_values": [ + false, + 30, + 0 + ] + } + ], + "links": [ + [ + 4, + 9, + 0, + 8, + 0, + "MODEL" + ], + [ + 5, + 9, + 1, + 8, + 1, + "CLIP" + ], + [ + 6, + 9, + 2, + 8, + 2, + "VAE" + ], + [ + 8, + 13, + 0, + 12, + 0, + "INT" + ], + [ + 9, + 12, + 0, + 11, + 1, + "BOOL" + ], + [ + 10, + 10, + 0, + 11, + 0, + "IMAGE" + ], + [ + 15, + 16, + 1, + 8, + 4, + "STRING" + ], + [ + 16, + 17, + 3, + 18, + 1, + "BOOL" + ], + [ + 17, + 18, + 0, + 20, + 0, + "IMAGE" + ], + [ + 18, + 18, + 0, + 19, + 0, + "IMAGE" + ], + [ + 24, + 14, + 0, + 8, + 3, + "IMAGE" + ], + [ + 25, + 8, + 0, + 15, + 0, + "IMAGE" + ], + [ + 26, + 11, + 0, + 14, + 0, + "IMAGE" + ], + [ + 27, + 17, + 1, + 22, + 0, + "FLOAT" + ], + [ + 28, + 22, + 0, + 8, + 5, + "FLOAT" + ], + [ + 29, + 17, + 1, + 23, + 0, + "FLOAT" + ], + [ + 30, + 23, + 0, + 14, + 1, + "FLOAT" + ] + ], + "groups": [ + { + "title": "Init Image", + "bounding": [ + -1730, + 215, + 716, + 713 + ], + "color": "#3f789e", + "locked": false + }, + { + "title": "Video Output", + "bounding": [ + -426, + 1176, + 1223, + 787 + ], + "color": "#3f789e", + "locked": false + }, + { + "title": "Animation", + "bounding": [ + -473, + 363, + 900, + 718 + ], + "color": "#3f789e", + "locked": false + } + ], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file From abf1e82adb9fac8cd70d5c409baad55309ef6fe1 Mon Sep 17 00:00:00 2001 From: melMass Date: Sun, 23 Jul 2023 03:13:54 +0200 Subject: [PATCH 52/70] =?UTF-8?q?fix:=20=F0=9F=94=A5=20remove=20notice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit we don't use this anymore --- nodes/image_processing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nodes/image_processing.py b/nodes/image_processing.py index acbb3c0..e38b7c2 100644 --- a/nodes/image_processing.py +++ b/nodes/image_processing.py @@ -17,10 +17,10 @@ import os import comfy.model_management as model_management -try: - from cv2.ximgproc import guidedFilter -except ImportError: - log.warning("cv2.ximgproc.guidedFilter not found, use opencv-contrib-python") +# try: +# from cv2.ximgproc import guidedFilter +# except ImportError: +# log.warning("cv2.ximgproc.guidedFilter not found, use opencv-contrib-python") class ColorCorrect: From 8d3cc39b72dff1b5eb61bf7e2e395753c138ec8a Mon Sep 17 00:00:00 2001 From: melMass Date: Sun, 23 Jul 2023 04:50:56 +0200 Subject: [PATCH 53/70] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20Unsplash=20Imag?= =?UTF-8?q?e=20node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/fun.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/nodes/fun.py b/nodes/fun.py index d63cce2..000214d 100644 --- a/nodes/fun.py +++ b/nodes/fun.py @@ -1,6 +1,7 @@ import qrcode from ..utils import pil2tensor from PIL import Image +from ..log import log # class MtbExamples: # """MTB Example Images""" @@ -50,6 +51,57 @@ from PIL import Image # return m.digest().hex() +class UnsplashImage: + """Unsplash Image given a keyword and a size""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "width": ("INT", {"default": 512, "max": 8096, "min": 0, "step": 1}), + "height": ("INT", {"default": 512, "max": 8096, "min": 0, "step": 1}), + "random_seed": ("INT", {"default": 0, "max": 1e5, "min": 0, "step": 1}), + }, + "optional": { + "keyword": ("STRING", {"default": "nature"}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "do_unsplash_image" + CATEGORY = "mtb/generate" + + def do_unsplash_image(self, width, height, random_seed, keyword=None): + import requests + import io + + base_url = "https://source.unsplash.com/random/" + + if width and height: + base_url += f"/{width}x{height}" + + if keyword: + keyword = keyword.replace(" ", "%20") + base_url += f"?{keyword}&{random_seed}" + else: + base_url += f"?&{random_seed}" + try: + log.debug(f"Getting unsplash image from {base_url}") + response = requests.get(base_url) + response.raise_for_status() + + image = Image.open(io.BytesIO(response.content)) + return ( + pil2tensor( + image, + ), + ) + + except requests.exceptions.RequestException as e: + print("Error retrieving image:", e) + return (None,) + + class QrCode: """Basic QR Code generator""" @@ -112,5 +164,6 @@ class QrCode: __nodes__ = [ QrCode, + UnsplashImage # MtbExamples, ] From 21729b2784a50fcaf24a63ac283bdae475a53ce7 Mon Sep 17 00:00:00 2001 From: melMass Date: Sun, 23 Jul 2023 15:13:46 +0200 Subject: [PATCH 54/70] =?UTF-8?q?refactor:=20=E2=9A=A1=EF=B8=8F=20remove?= =?UTF-8?q?=20empty=20inits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/animation.py | 3 --- nodes/conditions.py | 6 ------ nodes/crop.py | 12 ------------ nodes/deep_bump.py | 3 --- nodes/fun.py | 3 --- nodes/image_interpolation.py | 6 ------ nodes/image_processing.py | 18 ------------------ nodes/io.py | 3 --- nodes/latent_processing.py | 6 +++--- nodes/mask.py | 3 --- nodes/number.py | 12 ++---------- 11 files changed, 5 insertions(+), 70 deletions(-) diff --git a/nodes/animation.py b/nodes/animation.py index 14d564e..da22df1 100644 --- a/nodes/animation.py +++ b/nodes/animation.py @@ -4,9 +4,6 @@ from ..log import log class AnimationBuilder: """Convenient way to manage basic animation maths at the core of many of my workflows""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { diff --git a/nodes/conditions.py b/nodes/conditions.py index ffcf268..8046a98 100644 --- a/nodes/conditions.py +++ b/nodes/conditions.py @@ -10,9 +10,6 @@ import csv class SmartStep: """Utils to control the steps start/stop of the KAdvancedSampler in percentage""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -62,9 +59,6 @@ class StylesLoader: options = {} - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): input_dir = Path(folder_paths.base_path) / "styles" diff --git a/nodes/crop.py b/nodes/crop.py index fc7751a..89a397c 100644 --- a/nodes/crop.py +++ b/nodes/crop.py @@ -9,9 +9,6 @@ from ..log import log class Bbox: """The bounding box (BBOX) custom type used by other nodes""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -42,9 +39,6 @@ class Bbox: class BboxFromMask: """From a mask extract the bounding box""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -116,9 +110,6 @@ class Crop: The BBOX input takes precedence over the tuple input """ - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -205,9 +196,6 @@ class Uncrop: The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type The BBOX input takes precedence over the tuple input""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { diff --git a/nodes/deep_bump.py b/nodes/deep_bump.py index 8be4836..1444d35 100644 --- a/nodes/deep_bump.py +++ b/nodes/deep_bump.py @@ -241,9 +241,6 @@ def normals_to_height(normals_img, seamless, progress_callback): class DeepBump: """Normal & height maps generation from single pictures""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { diff --git a/nodes/fun.py b/nodes/fun.py index 000214d..d12af8f 100644 --- a/nodes/fun.py +++ b/nodes/fun.py @@ -105,9 +105,6 @@ class UnsplashImage: class QrCode: """Basic QR Code generator""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { diff --git a/nodes/image_interpolation.py b/nodes/image_interpolation.py index 94d3ac8..2fcb3f7 100644 --- a/nodes/image_interpolation.py +++ b/nodes/image_interpolation.py @@ -149,9 +149,6 @@ class LoadFilmModel: class FilmInterpolation: """Google Research FILM frame interpolation for large motion""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -214,9 +211,6 @@ class FilmInterpolation: class ConcatImages: """Add images to batch""" - def __init__(self): - pass - RETURN_TYPES = ("IMAGE",) FUNCTION = "concat_images" CATEGORY = "mtb/image" diff --git a/nodes/image_processing.py b/nodes/image_processing.py index e38b7c2..04f04e1 100644 --- a/nodes/image_processing.py +++ b/nodes/image_processing.py @@ -26,9 +26,6 @@ import comfy.model_management as model_management class ColorCorrect: """Various color correction methods""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -187,9 +184,6 @@ class ColorCorrect: class ImageCompare: """Compare two images and return a difference image""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -252,9 +246,6 @@ class LoadImageFromUrl: class Blur: """Blur an image using a Gaussian filter.""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -312,9 +303,6 @@ class Blur: class MaskToImage: """Converts a mask (alpha) to an RGB image with a color and background""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -383,9 +371,6 @@ class ColoredImage: class ImagePremultiply: """Premultiply image with mask""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -431,9 +416,6 @@ class ImagePremultiply: class ImageResizeFactor: """Extracted mostly from WAS Node Suite, with a few edits (most notably multiple image support) and less features.""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { diff --git a/nodes/io.py b/nodes/io.py index 873d284..d31d27e 100644 --- a/nodes/io.py +++ b/nodes/io.py @@ -12,9 +12,6 @@ import numpy as np class ExportToProres: """Export to ProRes 4444 (Experimental)""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { diff --git a/nodes/latent_processing.py b/nodes/latent_processing.py index 63e3ad1..3c5951e 100644 --- a/nodes/latent_processing.py +++ b/nodes/latent_processing.py @@ -1,9 +1,8 @@ import torch + class LatentLerp: """Linear interpolation (blend) between two latent vectors""" - def __init__(self): - pass @classmethod def INPUT_TYPES(cls): @@ -28,6 +27,7 @@ class LatentLerp: return (a,) + __nodes__ = [ LatentLerp, -] \ No newline at end of file +] diff --git a/nodes/mask.py b/nodes/mask.py index 56af63f..64e5119 100644 --- a/nodes/mask.py +++ b/nodes/mask.py @@ -7,9 +7,6 @@ import comfy.utils class ImageRemoveBackgroundRembg: """Removes the background from the input using Rembg.""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { diff --git a/nodes/number.py b/nodes/number.py index 529c15d..dc1be4b 100644 --- a/nodes/number.py +++ b/nodes/number.py @@ -25,9 +25,6 @@ class IntToBool: class IntToNumber: """Node addon for the WAS Suite. Converts a "comfy" INT to a NUMBER.""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -56,9 +53,6 @@ class IntToNumber: class FloatToNumber: """Node addon for the WAS Suite. Converts a "comfy" FLOAT to a NUMBER.""" - def __init__(self): - pass - @classmethod def INPUT_TYPES(cls): return { @@ -83,13 +77,11 @@ class FloatToNumber: def float_to_number(self, float): return (float,) - - return (int,) + __nodes__ = [ FloatToNumber, IntToBool, IntToNumber, - -] \ No newline at end of file +] From 55c9736a9b2ca036926be4b06406121bfb9ebad2 Mon Sep 17 00:00:00 2001 From: melMass Date: Sun, 23 Jul 2023 16:43:25 +0200 Subject: [PATCH 55/70] =?UTF-8?q?fix:=20=F0=9F=90=9B=20caching=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fonts and styles where searched for each rerun. This makes it require a restart to update either but it's not a big deal in these cases IMO. thanks to @ltdrdata for finding this issue! --- __init__.py | 2 +- nodes/conditions.py | 71 ++++++++++++++++++++++++++------------------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/__init__.py b/__init__.py index 2423e2b..de9288e 100644 --- a/__init__.py +++ b/__init__.py @@ -185,7 +185,7 @@ async def set_debug(request): if enabled: os.environ["MTB_DEBUG"] = "true" log.setLevel(logging.DEBUG) - log.debug("Debug mode set") + log.debug("Debug mode set from API (/mtb/debug POST route)") else: if "MTB_DEBUG" in os.environ: diff --git a/nodes/conditions.py b/nodes/conditions.py index 8046a98..71eeef0 100644 --- a/nodes/conditions.py +++ b/nodes/conditions.py @@ -1,5 +1,5 @@ from ..utils import pil2tensor -from ..utils import here +from ..utils import here, comfy_dir from ..log import log import folder_paths from pathlib import Path @@ -61,25 +61,26 @@ class StylesLoader: @classmethod def INPUT_TYPES(cls): - input_dir = Path(folder_paths.base_path) / "styles" - if not input_dir.exists(): - install_default_styles() + if not cls.options: + input_dir = Path(folder_paths.base_path) / "styles" + if not input_dir.exists(): + install_default_styles() + + if not (files := [f for f in input_dir.iterdir() if f.suffix == ".csv"]): + log.warn( + "No styles found in the styles folder, place at least one csv file in the styles folder at the root of ComfyUI (for instance ComfyUI/styles/mystyle.csv)" + ) + + for file in files: + with open(file, "r", encoding="utf8") as f: + parsed = csv.reader(f) + for row in parsed: + log.debug(f"Adding style {row[0]}") + cls.options[row[0]] = (row[1], row[2]) + + else: + log.debug(f"Using cached styles (count: {len(cls.options)})") - if not (files := [f for f in input_dir.iterdir() if f.suffix == ".csv"]): - log.error( - "No styles found in the styles folder, place at least one csv file in the styles folder" - ) - return { - "required": { - "style_name": (["error"],), - } - } - for file in files: - with open(file, "r", encoding="utf8") as f: - parsed = csv.reader(f) - for row in parsed: - log.debug(f"Adding style {row[0]}") - cls.options[row[0]] = (row[1], row[2]) return { "required": { "style_name": (list(cls.options.keys()),), @@ -106,24 +107,34 @@ class TextToImage: fonts = {} def __init__(self): + # - This is executed when the graph is executed, we could conditionaly reload fonts there pass @classmethod - def INPUT_TYPES(cls): - fonts = list(Path(folder_paths.base_path).glob("**/*.ttf")) + def CACHE_FONTS(cls): + font_extensions = ["*.ttf", "*.otf", "*.woff", "*.woff2", "*.eot"] + fonts = [] + + for extension in font_extensions: + fonts.extend(comfy_dir.glob(f"**/{extension}")) + if not fonts: - log.error( - "No fonts found in the fonts folder, place at least one ttf file in the fonts folder" + log.warn( + "> No fonts found in the comfy folder, place at least one font file somewhere in ComfyUI's hierarchy" ) - return { - "required": { - "font": (["error"],), - } - } + else: + log.debug(f"> Found {len(fonts)} fonts") + for font in fonts: log.debug(f"Adding font {font}") cls.fonts[font.stem] = font.as_posix() + @classmethod + def INPUT_TYPES(cls): + if not cls.fonts: + cls.CACHE_FONTS() + else: + log.debug(f"Using cached fonts (count: {len(cls.fonts)})") return { "required": { "text": ( @@ -137,11 +148,11 @@ class TextToImage: ), "font_size": ( "INT", - {"default": 12, "min": 1, "max": 100, "step": 1}, + {"default": 12, "min": 1, "max": 2500, "step": 1}, ), "width": ( "INT", - {"default": 512, "min": 1, "max": 1000, "step": 1}, + {"default": 512, "min": 1, "max": 8096, "step": 1}, ), "height": ( "INT", From 50d51c70d04e49e9df524975c171288c0fc0b20f Mon Sep 17 00:00:00 2001 From: melMass Date: Sun, 23 Jul 2023 17:11:09 +0200 Subject: [PATCH 56/70] =?UTF-8?q?fix:=20=F0=9F=8E=A8=20improve=20a=20bit?= =?UTF-8?q?=20the=20HTML=20response=20of=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 24 +++++++++++++-- endpoint.py | 32 ++++++++++++++++++- html/style.css | 83 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 134 insertions(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index de9288e..81a6434 100644 --- a/__init__.py +++ b/__init__.py @@ -163,13 +163,31 @@ from aiohttp import web from importlib import reload import logging -endlog = mklog("endpoint") +endlog = mklog("mtb endpoint") @PromptServer.instance.routes.get("/mtb/status") async def get_full_library(request): - files = [] - endlog.debug("Getting status") + from . import endpoint + + reload(endpoint) + + endlog.debug("Getting node registration status") + # Check if the request prefers HTML content + if "text/html" in request.headers.get("Accept", ""): + # # Return an HTML page + html_response = endpoint.render_table( + NODE_CLASS_MAPPINGS_DEBUG, title="Registered" + ) + html_response += endpoint.render_table( + {k: "-" for k in failed}, title="Failed to load" + ) + + return web.Response( + text=endpoint.render_base_template("MTB", html_response), + content_type="text/html", + ) + return web.json_response( { "registered": NODE_CLASS_MAPPINGS_DEBUG, diff --git a/endpoint.py b/endpoint.py index 1da1315..f75aab6 100644 --- a/endpoint.py +++ b/endpoint.py @@ -1,6 +1,34 @@ from .utils import here +def render_table(table_dict, sort=True, title=None): + table_rows = "" + table_dict = sorted( + table_dict.items(), key=lambda item: item[0] + ) # Sort the dictionary by keys + + for name, description in table_dict: + table_rows += f"{name}{description}" + + html_response = f""" +
+ {"" if title is None else f"

{title}

"} + + + + + + + + + {table_rows} + +
NameDescription
+
+ """ + return html_response + + def render_base_template(title, content): css_content = "" css_path = here / "html" / "style.css" @@ -20,8 +48,10 @@ def render_base_template(title, content):
+ Back to Comfy + {github_icon_svg} diff --git a/html/style.css b/html/style.css index 9c10a3e..00af383 100644 --- a/html/style.css +++ b/html/style.css @@ -6,6 +6,87 @@ html { color: whitesmoke; } +a { + color: whitesmoke; + +} + +.table-container { + width: 70%; + height: 100%; + overflow: auto; +} + +table { + + border-collapse: collapse; +} + +th, +td { + padding: 10px; + text-align: left; +} + +th { + background-color: rgb(45, 45, 45); + /* Light gray background for header row */ + font-weight: bold; +} + +tr:nth-child(even) { + background-color: rgb(45, 45, 45); + /* Alternate row background color */ +} + +tr:hover { + background-color: #797979; + /* Highlight color on hover */ +} + +td:nth-child(2) { + /* Applies to the second column (Description) */ + width: 80%; + /* Adjust the width as needed */ + word-wrap: break-word; + /* Allow long words to be broken and wrapped to the next line */ +} + +.mtb_logo { + display: flex; + flex-direction: column; + align-items: center; +} + +/* Styling for WebKit-based browsers (Chrome, Edge) */ +.table-container::-webkit-scrollbar { + width: 10px; + /* Set the width of the scrollbar */ +} + +.table-container::-webkit-scrollbar-thumb { + background-color: #797979; + /* Color of the scrollbar thumb */ +} + +/* Styling for Firefox */ +.table-container { + scrollbar-width: thin; + /* Set the width of the scrollbar */ +} + +.table-container::-webkit-scrollbar-thumb { + background-color: #797979; + /* Color of the scrollbar thumb */ +} + +/* Optionally, you can also style the scrollbar track (background) */ +.table-container::-webkit-scrollbar-track { + background-color: #f2f2f2; +} + + + body { margin: 0; padding: 0; @@ -18,7 +99,7 @@ body { .title { font-size: 2.5em; font-weight: 700; - margin: 1em; + } header { From bbdac97e49af4e90d22eeec3f63b96ecc126ffcf Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Mon, 24 Jul 2023 17:43:44 +0200 Subject: [PATCH 57/70] =?UTF-8?q?docs:=20=F0=9F=93=9D=20added=20lang=20lin?= =?UTF-8?q?ks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index fb7506f..6b8567a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ ## MTB Nodes +**Translated Readme (using DeepTranslate, PRs are welcome)**: +![image](https://github.com/melMass/comfy_mtb/assets/7041726/f8429c14-3521-4e28-82a3-863d781976c0) +[日本語による説明](./README-JP.md) +![image](https://github.com/melMass/comfy_mtb/assets/7041726/d5cc1fdd-2820-4a5c-b2d7-482f1c222063) +[中文说明](./README-CN.md) + Buy Me A Coffee Welcome to the MTB Nodes project! This codebase is open for you to explore and utilize as you wish. Its primary purpose is to build proof-of-concepts (POCs) for implementation in [MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs). Many nodes in this project are inspired by existing community contributions or built-in functionalities. From 3801a443bc1e89c70fdb35ce0b1724d86fa22928 Mon Sep 17 00:00:00 2001 From: melMass Date: Mon, 24 Jul 2023 20:20:26 +0200 Subject: [PATCH 58/70] =?UTF-8?q?refactor:=20=E2=9C=A8=20cleaned=20up=20fr?= =?UTF-8?q?ontend=20code=20a=20bit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierrc | 6 + __init__.py | 17 +- endpoint.py | 4 + web/comfy_shared.js | 437 +++++----- web/debug.js | 159 ++-- web/imageFeed.js | 538 ++++++------ web/mtb_widgets.js | 1913 +++++++++++++++++++------------------------ web/notify.js | 10 + 8 files changed, 1479 insertions(+), 1605 deletions(-) create mode 100644 .prettierrc create mode 100644 web/notify.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..bc6c48e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false +} \ No newline at end of file diff --git a/__init__.py b/__init__.py index 81a6434..8a7e7a7 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,12 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +### +# File: __init__.py +# Project: comfy_mtb +# Author: Mel Massadian +# Copyright (c) 2023 Mel Massadian +# +### import os os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true" @@ -5,6 +14,7 @@ os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true" import traceback from .log import log, blue_text, cyan_text, get_summary, get_label from .utils import here +from .utils import comfy_dir import importlib import os import ast @@ -89,7 +99,7 @@ def load_nodes(): # - REGISTER WEB EXTENSIONS -web_extensions_root = utils.comfy_dir / "web" / "extensions" +web_extensions_root = comfy_dir / "web" / "extensions" web_mtb = web_extensions_root / "mtb" if web_mtb.exists(): @@ -158,12 +168,11 @@ log.info( # - ENDPOINT from server import PromptServer -from .log import mklog, log +from .log import log from aiohttp import web from importlib import reload import logging - -endlog = mklog("mtb endpoint") +from .endpoint import endlog @PromptServer.instance.routes.get("/mtb/status") diff --git a/endpoint.py b/endpoint.py index f75aab6..a38e269 100644 --- a/endpoint.py +++ b/endpoint.py @@ -1,5 +1,9 @@ from .utils import here +from aiohttp import web +from .log import mklog +import os +endlog = mklog("mtb endpoint") def render_table(table_dict, sort=True, title=None): table_rows = "" diff --git a/web/comfy_shared.js b/web/comfy_shared.js index f6a2232..f44e10c 100644 --- a/web/comfy_shared.js +++ b/web/comfy_shared.js @@ -1,282 +1,309 @@ -import { app } from "/scripts/app.js"; +import { app } from '/scripts/app.js' export const log = (...args) => { - if (window.MTB_DEBUG) { - console.debug(...args); - } - + if (window.MTB?.DEBUG) { + console.debug(...args) + } } //- WIDGET UTILS -export const CONVERTED_TYPE = "converted-widget"; +export const CONVERTED_TYPE = 'converted-widget' -export function offsetDOMWidget(widget, ctx, node, widgetWidth, widgetY, height) { - 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 + widgetY); +export function offsetDOMWidget( + widget, + ctx, + node, + widgetWidth, + widgetY, + height +) { + 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 + widgetY) - const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) - Object.assign(widget.inputEl.style, { - transformOrigin: "0 0", - transform: scale, - left: `${transform.a + transform.e}px`, - top: `${transform.d + transform.f}px`, - width: `${widgetWidth - (margin * 2)}px`, - // height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`, - height: `${(height || widget.parent?.inputHeight || 32) - (margin * 2)}px`, + const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) + Object.assign(widget.inputEl.style, { + transformOrigin: '0 0', + transform: scale, + left: `${transform.a + transform.e}px`, + top: `${transform.d + transform.f}px`, + width: `${widgetWidth - margin * 2}px`, + // height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`, + height: `${(height || widget.parent?.inputHeight || 32) - margin * 2}px`, - position: "absolute", - background: (!node.color) ? '' : node.color, - color: (!node.color) ? '' : 'white', - zIndex: app.graph._nodes.indexOf(node), - }) + position: 'absolute', + background: !node.color ? '' : node.color, + color: !node.color ? '' : 'white', + zIndex: app.graph._nodes.indexOf(node), + }) } /** * Extracts the type and link type from a widget config object. - * @param {*} config - * @returns + * @param {*} config + * @returns */ export function getWidgetType(config) { - // Special handling for COMBO so we restrict links based on the entries - let type = config[0]; - let linkType = type; - if (type instanceof Array) { - type = "COMBO"; - linkType = linkType.join(","); - } - return { type, linkType }; + // Special handling for COMBO so we restrict links based on the entries + let type = config[0] + let linkType = type + if (type instanceof Array) { + type = 'COMBO' + linkType = linkType.join(',') + } + return { type, linkType } } -export const dynamic_connection = (node, index, connected, connectionPrefix = "input_", connectionType = "PSDLAYER") => { - - // remove all non connected inputs - if (!connected && node.inputs.length > 1) { - log(`Removing input ${index} (${node.inputs[index].name})`) - if (node.widgets) { - const w = node.widgets.find((w) => w.name === node.inputs[index].name); - if (w) { - w.onRemove?.(); - node.widgets.length = node.widgets.length - 1 - } - } - node.removeInput(index) - - // make inputs sequential again - for (let i = 0; i < node.inputs.length; i++) { - node.inputs[i].label = `${connectionPrefix}${i + 1}` - } +export const dynamic_connection = ( + node, + index, + connected, + connectionPrefix = 'input_', + connectionType = 'PSDLAYER' +) => { + // remove all non connected inputs + if (!connected && node.inputs.length > 1) { + log(`Removing input ${index} (${node.inputs[index].name})`) + if (node.widgets) { + const w = node.widgets.find((w) => w.name === node.inputs[index].name) + if (w) { + w.onRemoved?.() + node.widgets.length = node.widgets.length - 1 + } } + node.removeInput(index) - // add an extra input - if (node.inputs[node.inputs.length - 1].link != undefined) { - log(`Adding input ${node.inputs.length + 1} (${connectionPrefix}${node.inputs.length + 1})`) - - node.addInput(`${connectionPrefix}${node.inputs.length + 1}`, connectionType) + // make inputs sequential again + for (let i = 0; i < node.inputs.length; i++) { + node.inputs[i].label = `${connectionPrefix}${i + 1}` } + } + // add an extra input + if (node.inputs[node.inputs.length - 1].link != undefined) { + log( + `Adding input ${node.inputs.length + 1} (${connectionPrefix}${ + node.inputs.length + 1 + })` + ) + + node.addInput( + `${connectionPrefix}${node.inputs.length + 1}`, + connectionType + ) + } } - /** * Appends a callback to the extra menu options of a given node type. - * @param {*} nodeType - * @param {*} cb + * @param {*} nodeType + * @param {*} cb */ export function 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 getOpts = nodeType.prototype.getExtraMenuOptions + nodeType.prototype.getExtraMenuOptions = function () { + const r = getOpts.apply(this, arguments) + cb.apply(this, arguments) + return r + } } -export function hideWidget(node, widget, suffix = "") { - widget.origType = widget.type; - widget.hidden = true - 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 - const { link } = node.inputs.find((i) => i.widget?.name === widget.name); - if (link == null) { - 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); - } +export function hideWidget(node, widget, suffix = '') { + widget.origType = widget.type + widget.hidden = true + 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 + const { link } = node.inputs.find((i) => i.widget?.name === widget.name) + if (link == null) { + 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) + } + } } export function showWidget(widget) { - widget.type = widget.origType; - widget.computeSize = widget.origComputeSize; - widget.serializeValue = widget.origSerializeValue; + widget.type = widget.origType + widget.computeSize = widget.origComputeSize + widget.serializeValue = widget.origSerializeValue - delete widget.origType; - delete widget.origComputeSize; - delete widget.origSerializeValue; + delete widget.origType + delete widget.origComputeSize + delete widget.origSerializeValue - // Hide any linked widgets, e.g. seed+seedControl - if (widget.linkedWidgets) { - for (const w of widget.linkedWidgets) { - showWidget(w); - } + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + showWidget(w) } + } } export function convertToWidget(node, widget) { - showWidget(widget); - const sz = node.size; - node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name)); + showWidget(widget) + const sz = node.size + node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name)) - for (const widget of node.widgets) { - widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT; - } + 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])]); + // Restore original size but grow if needed + node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]) } - export function convertToInput(node, widget, config) { - hideWidget(node, widget); + hideWidget(node, widget) - const { linkType } = getWidgetType(config); + const { linkType } = getWidgetType(config) - // Add input and store widget config for creating on primitive node - const sz = node.size; - node.addInput(widget.name, linkType, { - widget: { name: widget.name, config }, - }); + // Add input and store widget config for creating on primitive node + const sz = node.size + node.addInput(widget.name, linkType, { + widget: { name: widget.name, config }, + }) - for (const widget of node.widgets) { - widget.last_y += LiteGraph.NODE_SLOT_HEIGHT; - } + 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])]); + // Restore original size but grow if needed + node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]) } -export function hideWidgetForGood(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 - // const w = node.inputs?.find((i) => i.widget?.name === widget.name); - // if (w?.link == null) { - // return undefined; - // } - // return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; - // }; +export function hideWidgetForGood(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 + // const w = node.inputs?.find((i) => i.widget?.name === widget.name); + // if (w?.link == null) { + // 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) { - hideWidgetForGood(node, w, ":" + widget.name); - } + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + hideWidgetForGood(node, w, ':' + widget.name) } + } } export function fixWidgets(node) { - if (node.inputs) { - for (const input of node.inputs) { - log(input) - if (input.widget || node.widgets) { - // if (newTypes.includes(input.type)) { - const matching_widget = node.widgets.find((w) => w.name === input.name); - if (matching_widget) { + if (node.inputs) { + for (const input of node.inputs) { + log(input) + if (input.widget || node.widgets) { + // if (newTypes.includes(input.type)) { + const matching_widget = node.widgets.find((w) => w.name === input.name) + if (matching_widget) { + // if (matching_widget.hidden) { + // log(`Already hidden skipping ${matching_widget.name}`) + // continue + // } + const w = node.widgets.find((w) => w.name === matching_widget.name) + if (w && w.type != CONVERTED_TYPE) { + log(w) + log(`hidding ${w.name}(${w.type}) from ${node.type}`) + log(node) + hideWidget(node, w) + } else { + log(`converting to widget ${w}`) - - // if (matching_widget.hidden) { - // log(`Already hidden skipping ${matching_widget.name}`) - // continue - // } - const w = node.widgets.find((w) => w.name === matching_widget.name); - if (w && w.type != CONVERTED_TYPE) { - log(w) - log(`hidding ${w.name}(${w.type}) from ${node.type}`) - log(node) - hideWidget(node, w); - } else { - log(`converting to widget ${w}`) - - convertToWidget(node, input) - } - } - } + convertToWidget(node, input) + } } + } } + } } export function inner_value_change(widget, value, event = undefined) { - if (widget.type == "number" || widget.type == "BBOX") { - value = Number(value); - } else if (widget.type == "BOOL") { - value = Boolean(value) - } - widget.value = value; - if (widget.options && widget.options.property && node.properties[widget.options.property] !== undefined) { - node.setProperty(widget.options.property, value); - } - if (widget.callback) { - widget.callback(widget.value, app.canvas, node, pos, event); - } + if (widget.type == 'number' || widget.type == 'BBOX') { + value = Number(value) + } else if (widget.type == 'BOOL') { + value = Boolean(value) + } + widget.value = value + if ( + widget.options && + widget.options.property && + node.properties[widget.options.property] !== undefined + ) { + node.setProperty(widget.options.property, value) + } + if (widget.callback) { + widget.callback(widget.value, app.canvas, node, pos, event) + } } //- COLOR UTILS export function isColorBright(rgb, threshold = 240) { - const brightess = getBrightness(rgb) - return brightess > threshold + const brightess = getBrightness(rgb) + return brightess > threshold } function getBrightness(rgbObj) { - return Math.round(((parseInt(rgbObj[0]) * 299) + (parseInt(rgbObj[1]) * 587) + (parseInt(rgbObj[2]) * 114)) / 1000) + return Math.round( + (parseInt(rgbObj[0]) * 299 + + parseInt(rgbObj[1]) * 587 + + parseInt(rgbObj[2]) * 114) / + 1000 + ) } //- HTML / CSS UTILS export function defineClass(className, classStyles) { - const styleSheets = document.styleSheets; + const styleSheets = document.styleSheets - // Helper function to check if the class exists in a style sheet - function classExistsInStyleSheet(styleSheet) { - const rules = styleSheet.rules || styleSheet.cssRules; - for (const rule of rules) { - if (rule.selectorText === `.${className}`) { - return true; - } - } - return false; + // Helper function to check if the class exists in a style sheet + function classExistsInStyleSheet(styleSheet) { + const rules = styleSheet.rules || styleSheet.cssRules + for (const rule of rules) { + if (rule.selectorText === `.${className}`) { + return true + } } + return false + } - // Check if the class is already defined in any of the style sheets - let classExists = false; - for (const styleSheet of styleSheets) { - if (classExistsInStyleSheet(styleSheet)) { - classExists = true; - break; - } + // Check if the class is already defined in any of the style sheets + let classExists = false + for (const styleSheet of styleSheets) { + if (classExistsInStyleSheet(styleSheet)) { + classExists = true + break } + } - // If the class doesn't exist, add the new class definition to the first style sheet - if (!classExists) { - if (styleSheets[0].insertRule) { - styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0); - } else if (styleSheets[0].addRule) { - styleSheets[0].addRule(`.${className}`, classStyles, 0); - } + // If the class doesn't exist, add the new class definition to the first style sheet + if (!classExists) { + if (styleSheets[0].insertRule) { + styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0) + } else if (styleSheets[0].addRule) { + styleSheets[0].addRule(`.${className}`, classStyles, 0) } + } } diff --git a/web/debug.js b/web/debug.js index 00079e1..e29d2a3 100644 --- a/web/debug.js +++ b/web/debug.js @@ -1,80 +1,99 @@ -import { app } from "/scripts/app.js"; +/** + * File: debug.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +import { app } from '/scripts/app.js' import * as shared from '/extensions/mtb/comfy_shared.js' import { log } from '/extensions/mtb/comfy_shared.js' import { MtbWidgets } from '/extensions/mtb/mtb_widgets.js' // TODO: respect inputs order... - - app.registerExtension({ - name: "mtb.Debug", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === "Debug (mtb)") { - const onConnectionsChange = nodeType.prototype.onConnectionsChange; - nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) { - const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined; - // TODO: remove all widgets on disconnect once computed - shared.dynamic_connection(this, index, connected, "anything_", "*") + name: 'mtb.Debug', + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === 'Debug (mtb)') { + const onConnectionsChange = nodeType.prototype.onConnectionsChange + nodeType.prototype.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + const r = onConnectionsChange + ? onConnectionsChange.apply(this, arguments) + : undefined + // TODO: remove all widgets on disconnect once computed + shared.dynamic_connection(this, index, connected, 'anything_', '*') - //- infer type - if (link_info) { - const fromNode = this.graph._nodes.find((otherNode) => otherNode.id == link_info.origin_id); - const type = fromNode.outputs[link_info.origin_slot].type; - this.inputs[index].type = type; - // this.inputs[index].label = type.toLowerCase() - } - //- restore dynamic input - if (!connected) { - this.inputs[index].type = "*"; - this.inputs[index].label = `anything_${index + 1}` - } - } - - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (message) { - log(message) - onExecuted?.apply(this, arguments); - log(message) - if (this.widgets) { - // const pos = this.widgets.findIndex((w) => w.name === "anything_1"); - // if (pos !== -1) { - for (let i = 0; i < this.widgets.length; i++) { - this.widgets[i].onRemove?.(); - } - this.widgets.length = 0; - - } - let widgetI = 1 - if (message.text) { - for (const txt of message.text) { - const w = this.addCustomWidget(MtbWidgets.DEBUG_STRING(txt, widgetI)) - w.parent = this; - widgetI++; - } - } - if (message.b64_images) { - for (const img of message.b64_images) { - const w = this.addCustomWidget(MtbWidgets.DEBUG_IMG(img, widgetI)) - w.parent = this; - widgetI++; - } - // this.onResize?.(this.size); - // this.resize?.(this.size) - this.setSize(this.computeSize()) - }; - - this.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].canvas) { - this.widgets[y].canvas.remove(); - } - this.widgets[y].onRemove?.(); - } - } - } + //- infer type + if (link_info) { + const fromNode = this.graph._nodes.find( + (otherNode) => otherNode.id == link_info.origin_id + ) + const type = fromNode.outputs[link_info.origin_slot].type + this.inputs[index].type = type + // this.inputs[index].label = type.toLowerCase() } + //- restore dynamic input + if (!connected) { + this.inputs[index].type = '*' + this.inputs[index].label = `anything_${index + 1}` + } + } + + const onExecuted = nodeType.prototype.onExecuted + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments) + + const prefix = 'anything_' + + if (this.widgets) { + // const pos = this.widgets.findIndex((w) => w.name === "anything_1"); + // if (pos !== -1) { + for (let i = 0; i < this.widgets.length; i++) { + this.widgets[i].onRemoved?.() + } + this.widgets.length = 0 + } + let widgetI = 1 + if (message.text) { + for (const txt of message.text) { + const w = this.addCustomWidget( + MtbWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, txt) + ) + w.parent = this + widgetI++ + } + } + if (message.b64_images) { + for (const img of message.b64_images) { + const w = this.addCustomWidget( + MtbWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img) + ) + w.parent = this + widgetI++ + } + // this.onResize?.(this.size); + // this.resize?.(this.size) + this.setSize(this.computeSize()) + } + + this.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].canvas) { + this.widgets[y].canvas.remove() + } + this.widgets[y].onRemoved?.() + } + } + } } -} -); + }, +}) diff --git a/web/imageFeed.js b/web/imageFeed.js index e003988..18347e5 100644 --- a/web/imageFeed.js +++ b/web/imageFeed.js @@ -1,311 +1,313 @@ -import { api } from "/scripts/api.js"; -import { app } from "/scripts/app.js"; +/** + * File: imageFeed.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ // forked from pysssss's imageFeed.js -const styles = { - lighbox: { - position: "fixed", - top: 0, - left: 0, - width: "100vw", - height: "100vh", - background: "rgba(0,0,0,0.5)", - display: "none", - justifyContent: "center", - alignItems: "center", - zIndex: 999, - }, - lightboxBtn: (extra) => ({ - position: "absolute", - top: "50%", - background: "none", - border: "none", - color: "#fff", - zIndex: 9999999, - fontSize: "30px", - cursor: "pointer", - pointerEvents: "bounding-box", - ...extra, - }) - , - img_list: { +import { api } from '/scripts/api.js' +import { app } from '/scripts/app.js' - minHeight: "30px", - maxHeight: "300px", - width: "100vw", - position: "absolute", - bottom: 0, - zIndex: 9999999, - background: "#333", - overflow: "auto", - } +const styles = { + lighbox: { + position: 'fixed', + top: 0, + left: 0, + width: '100vw', + height: '100vh', + background: 'rgba(0,0,0,0.5)', + display: 'none', + justifyContent: 'center', + alignItems: 'center', + zIndex: 999, + }, + lightboxBtn: (extra) => ({ + position: 'absolute', + top: '50%', + background: 'none', + border: 'none', + color: '#fff', + zIndex: 9999999, + fontSize: '30px', + cursor: 'pointer', + pointerEvents: 'bounding-box', + ...extra, + }), + img_list: { + minHeight: '30px', + maxHeight: '300px', + width: '100vw', + position: 'absolute', + bottom: 0, + zIndex: 9999999, + background: '#333', + overflow: 'auto', + }, } -let currentImageIndex = 0; -const imageUrls = []; +let currentImageIndex = 0 +const imageUrls = [] let image_menu = null app.registerExtension({ - name: "mtb.ImageFeed", - setup: async () => { - // - HTML & CSS - //- lightbox - const lightboxContainer = document.createElement("div"); - Object.assign(lightboxContainer.style, styles.lighbox); + name: 'mtb.ImageFeed', + setup: async () => { + // - HTML & CSS + //- lightbox + const lightboxContainer = document.createElement('div') + Object.assign(lightboxContainer.style, styles.lighbox) - const lightboxImage = document.createElement("img"); - Object.assign(lightboxImage.style, { - maxHeight: "100%", - maxWidth: "100%", - borderRadius: "5px", - }); + const lightboxImage = document.createElement('img') + Object.assign(lightboxImage.style, { + maxHeight: '100%', + maxWidth: '100%', + borderRadius: '5px', + }) - // previous and next buttons - const lightboxPrevBtn = document.createElement("button"); - const lightboxNextBtn = document.createElement("button"); + // previous and next buttons + const lightboxPrevBtn = document.createElement('button') + const lightboxNextBtn = document.createElement('button') - lightboxPrevBtn.textContent = "❮"; - lightboxNextBtn.textContent = "❯"; + lightboxPrevBtn.textContent = '❮' + lightboxNextBtn.textContent = '❯' - Object.assign(lightboxPrevBtn.style, styles.lightboxBtn({ left: "0%" })); - Object.assign(lightboxNextBtn.style, styles.lightboxBtn({ right: "0%" })); + Object.assign(lightboxPrevBtn.style, styles.lightboxBtn({ left: '0%' })) + Object.assign(lightboxNextBtn.style, styles.lightboxBtn({ right: '0%' })) - // close button - const lightboxCloseBtn = document.createElement("button"); - Object.assign(lightboxCloseBtn.style, styles.lightboxBtn({ right: "0", top: "0" })); - lightboxCloseBtn.textContent = "❌"; + // close button + const lightboxCloseBtn = document.createElement('button') + Object.assign( + lightboxCloseBtn.style, + styles.lightboxBtn({ right: '0', top: '0' }) + ) + lightboxCloseBtn.textContent = '❌' - const lightboxButtons = document.createElement("div"); - Object.assign(lightboxButtons.style, { - position: "absolute", - top: "0%", - right: "0%", - // transform: "translate(50%, -50%)", - height: "100%", - width: "100%", - background: "none", - border: "none", - color: "#fff", - fontSize: "30px", - cursor: "pointer", - pointerEvents: "none", - }); + const lightboxButtons = document.createElement('div') + Object.assign(lightboxButtons.style, { + position: 'absolute', + top: '0%', + right: '0%', + // transform: "translate(50%, -50%)", + height: '100%', + width: '100%', + background: 'none', + border: 'none', + color: '#fff', + fontSize: '30px', + cursor: 'pointer', + pointerEvents: 'none', + }) - lightboxButtons.append(lightboxPrevBtn, lightboxNextBtn, lightboxCloseBtn); - lightboxContainer.append(lightboxButtons, lightboxImage); + lightboxButtons.append(lightboxPrevBtn, lightboxNextBtn, lightboxCloseBtn) + lightboxContainer.append(lightboxButtons, lightboxImage) - //- image list - const imageListContainer = document.createElement("div"); - Object.assign(imageListContainer.style, styles.img_list); + //- image list + const imageListContainer = document.createElement('div') + Object.assign(imageListContainer.style, styles.img_list) + const createImgListBtn = (text, style) => { + const btn = document.createElement('button') + btn.type = 'button' + btn.textContent = text + Object.assign(btn.style, { + ...style, + border: 'none', + color: '#fff', + background: 'none', + height: '20px', + cursor: 'pointer', + position: 'absolute', + top: '5px', + fontSize: '12px', + lineHeight: '12px', + }) + imageListContainer.append(btn) + return btn + } + const showBtn = document.createElement('button') + const closeBtn = createImgListBtn('❌', { + width: '20px', + textIndent: '-4px', + right: '5px', + }) + const loadButton = createImgListBtn('Load Session History', { + right: '90px', + }) + const clearButton = createImgListBtn('Clear', { + right: '30px', + }) - const createImgListBtn = (text, style) => { - const btn = document.createElement("button"); - btn.type = "button"; - btn.textContent = text; - Object.assign(btn.style, { - ...style, - border: "none", - color: "#fff", - background: "none", - height: "20px", - cursor: "pointer", - position: "absolute", - top: "5px", - fontSize: "12px", - lineHeight: "12px", - }); - imageListContainer.append(btn); - return btn; - } - const showBtn = document.createElement("button"); - const closeBtn = createImgListBtn("❌", { - width: "20px", - textIndent: "-4px", - right: "5px", - }); - const loadButton = createImgListBtn("Load Session History", { - right: "90px", - }); - const clearButton = createImgListBtn("Clear", { - right: "30px", - }); + //- tools popup button + showBtn.classList.add('comfy-settings-btn') + Object.assign(showBtn.style, { + right: '16px', + cursor: 'pointer', + display: 'none', + }) + //- append to DOM + document.body.append(imageListContainer) - //- tools popup button - showBtn.classList.add("comfy-settings-btn"); - Object.assign(showBtn.style, { - right: "16px", - cursor: "pointer", - display: "none", - }); + showBtn.textContent = '🖼️' + showBtn.onclick = () => { + imageListContainer.style.display = 'block' + showBtn.style.display = 'none' + } + document.querySelector('.comfy-settings-btn').after(showBtn) + document.querySelector('.comfy-settings-btn').after(lightboxContainer) - //- append to DOM - document.body.append(imageListContainer); + // for (const { output } of history) { + // if (output?.images) { + // for (const src of output.images) { + // const img = document.createElement("img"); + // const but = document.createElement("button"); + //- callbacks + closeBtn.onclick = () => { + imageListContainer.style.display = 'none' + showBtn.style.display = 'unset' + } - showBtn.textContent = "🖼️"; - showBtn.onclick = () => { - imageListContainer.style.display = "block"; - showBtn.style.display = "none"; - }; - document.querySelector(".comfy-settings-btn").after(showBtn); - document.querySelector(".comfy-settings-btn").after(lightboxContainer); + clearButton.onclick = () => { + imageListContainer.replaceChildren(closeBtn, clearButton, loadButton) + } + lightboxNextBtn.onclick = () => { + currentImageIndex = (currentImageIndex + 1) % imageUrls.length + const imageUrl = imageUrls[currentImageIndex] + lightboxImage.src = imageUrl + } + // Modify the lightboxPrevBtn onclick callback + lightboxPrevBtn.onclick = () => { + currentImageIndex = + (currentImageIndex - 1 + imageUrls.length) % imageUrls.length + const imageUrl = imageUrls[currentImageIndex] + lightboxImage.src = imageUrl + } - // for (const { output } of history) { - // if (output?.images) { - // for (const src of output.images) { - // const img = document.createElement("img"); - // const but = document.createElement("button"); + lightboxCloseBtn.onclick = () => { + lightboxContainer.style.display = 'none' + } + lightboxImage.onclick = lightboxNextBtn.onclick + /** + * This is the function that creates the image buttons for the image list + * They are wrapped in a button so that they can be clicked and open + * the image in the lightbox. + * @param {*} src + */ + const createImageBtn = (src) => { + console.debug(`making image ${src.filename}`) + const img = document.createElement('img') + const but = document.createElement('button') - //- callbacks - closeBtn.onclick = () => { - imageListContainer.style.display = "none"; - showBtn.style.display = "unset"; - }; + Object.assign(but.style, { + height: '120px', + width: '120px', + }) + Object.assign(img.style, { + width: '100%', + height: '100%', + objectFit: 'scale-down', + }) - clearButton.onclick = () => { - imageListContainer.replaceChildren(closeBtn, clearButton, loadButton); - } + img.src = `/view?filename=${encodeURIComponent(src.filename)}&type=${ + src.type + }&subfolder=${encodeURIComponent(src.subfolder)}` - lightboxNextBtn.onclick = () => { - currentImageIndex = (currentImageIndex + 1) % imageUrls.length; - const imageUrl = imageUrls[currentImageIndex]; - lightboxImage.src = imageUrl; - }; + imageUrls.push(img.src) - // Modify the lightboxPrevBtn onclick callback - lightboxPrevBtn.onclick = () => { - currentImageIndex = (currentImageIndex - 1 + imageUrls.length) % imageUrls.length; - const imageUrl = imageUrls[currentImageIndex]; - lightboxImage.src = imageUrl; - }; + console.debug(img.src) + but.onclick = () => { + lightboxContainer.style.display = 'flex' + // add the same image to the lightbox + lightboxImage.src = img.src + // lighboxContainer.replaceChildren(lightboxButtons, img); + } - lightboxCloseBtn.onclick = () => { - lightboxContainer.style.display = "none"; - }; - lightboxImage.onclick = lightboxNextBtn.onclick; - /** - * This is the function that creates the image buttons for the image list - * They are wrapped in a button so that they can be clicked and open - * the image in the lightbox. - * @param {*} src - */ - const createImageBtn = (src) => { - console.debug(`making image ${src.filename}`); - const img = document.createElement("img"); - const but = document.createElement("button"); + // add right click menu + but.addEventListener('contextmenu', (e) => { + e.preventDefault() - Object.assign(but.style, { - height: "120px", - width: "120px", - }); - Object.assign(img.style, { - width: "100%", - height: "100%", - objectFit: "scale-down", - }); + if (image_menu) { + image_menu.remove() + } - img.src = `/view?filename=${encodeURIComponent(src.filename)}&type=${src.type}&subfolder=${encodeURIComponent( - src.subfolder - )}`; + image_menu = document.createElement('div') + Object.assign(image_menu.style, { + position: 'absolute', + top: `${e.clientY}px`, + left: `${e.clientX}px`, + background: '#333', + color: '#fff', + padding: '5px', + borderRadius: '5px', + zIndex: 999, + }) + const load_img = document.createElement('button') + load_img.textContent = 'Load' + load_img.onclick = () => { + app.handleFile(img.src) + } - imageUrls.push(img.src); + image_menu.appendChild(load_img) + document.body.appendChild(image_menu) + }) - console.debug(img.src) + but.append(img) + imageListContainer.prepend(but) + } - but.onclick = () => { - lightboxContainer.style.display = "flex"; - // add the same image to the lightbox - lightboxImage.src = img.src; - // lighboxContainer.replaceChildren(lightboxButtons, img); + loadButton.onclick = async () => { + const all_history = await api.getHistory() + for (const history of all_history.History) { + if (history.outputs) { + for (const key of Object.keys(history.outputs)) { + console.debug(key) + for (const im of history.outputs[key].images) { + console.debug(im) + createImageBtn(im) + } + } + // for (const src of outputs.outputs.images) { + // console.debug(src) + // makeImage(`${src.subfolder}/${src.filename}`) + // } + } + } + } - }; + ///////------- - // add right click menu - but.addEventListener("contextmenu", (e) => { - e.preventDefault(); + // const all_history = await api.getHistory() + // for (const history of all_history.History) { + // if (history.outputs) { + // for (const key of Object.keys(history.outputs)) { + // for (const im of history.outputs[key].images) { + // makeImage(im) + // } + // } + // // for (const src of outputs.outputs.images) { + // // console.debug(src) + // // makeImage(`${src.subfolder}/${src.filename}`) + // // } + // } + // } - if (image_menu) { - image_menu.remove(); - } - - image_menu = document.createElement("div"); - Object.assign(image_menu.style, { - position: "absolute", - top: `${e.clientY}px`, - left: `${e.clientX}px`, - background: "#333", - color: "#fff", - padding: "5px", - borderRadius: "5px", - zIndex: 999, - }); - const load_img = document.createElement("button"); - load_img.textContent = "Load"; - load_img.onclick = () => { - app.handleFile(img.src) - - } - - image_menu.appendChild(load_img) - document.body.appendChild(image_menu) - }) - - but.append(img) - imageListContainer.prepend(but) - }; - - - loadButton.onclick = async () => { - const all_history = await api.getHistory(); - for (const history of all_history.History) { - if (history.outputs) { - for (const key of Object.keys(history.outputs)) { - console.debug(key) - for (const im of history.outputs[key].images) { - console.debug(im) - createImageBtn(im) - } - } - // for (const src of outputs.outputs.images) { - // console.debug(src) - // makeImage(`${src.subfolder}/${src.filename}`) - // } - } - } - } - - ///////------- - - // const all_history = await api.getHistory() - // for (const history of all_history.History) { - // if (history.outputs) { - // for (const key of Object.keys(history.outputs)) { - // for (const im of history.outputs[key].images) { - // makeImage(im) - // } - // } - // // for (const src of outputs.outputs.images) { - // // console.debug(src) - // // makeImage(`${src.subfolder}/${src.filename}`) - // // } - // } - // } - - //- Hook into the API - api.addEventListener("executed", ({ detail }) => { - if (detail?.output?.images) { - for (const src of detail.output.images) { - console.debug(`Adding ${src} to image feed`) - createImageBtn(src) - } - } - }) - } + //- Hook into the API + api.addEventListener('executed', ({ detail }) => { + if (detail?.output?.images) { + for (const src of detail.output.images) { + console.debug(`Adding ${src} to image feed`) + createImageBtn(src) + } + } + }) + }, }) diff --git a/web/mtb_widgets.js b/web/mtb_widgets.js index 3261636..ab23434 100644 --- a/web/mtb_widgets.js +++ b/web/mtb_widgets.js @@ -1,1081 +1,878 @@ -import { app } from "/scripts/app.js"; +/** + * File: mtb_widgets.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +import { app } from '/scripts/app.js' import parseCss from '/extensions/mtb/extern/parse-css.js' import * as shared from '/extensions/mtb/comfy_shared.js' +import { log } from '/extensions/mtb/comfy_shared.js' +import { api } from '/scripts/api.js' -import { api } from "/scripts/api.js"; - -import { ComfyWidgets } from "/scripts/widgets.js"; - -const newTypes = ["BOOL", "COLOR", "BBOX"] - +const newTypes = ['BOOL', 'COLOR', 'BBOX'] export const MtbWidgets = { - BBOX: (key, val) => { - /** @type {import("./types/litegraph").IWidget} */ - const widget = { - name: key, - type: "BBOX", - // options: val, - y: 0, - value: val?.default || [0, 0, 0, 0], - options: {}, - - draw: function (ctx, - node, - widget_width, - widgetY, - height) { - const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5; - - const show_text = true; - const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; - const background_color = LiteGraph.WIDGET_BGCOLOR; - const text_color = LiteGraph.WIDGET_TEXT_COLOR; - const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; - const H = LiteGraph.NODE_WIDGET_HEIGHT; - - var margin = 15; - var numWidgets = 4; // Number of stacked widgets - - if (hide) return; - - for (let i = 0; i < numWidgets; i++) { - let currentY = widgetY + i * (H + margin); // Adjust Y position for each widget - - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if (show_text) - ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]); - else - ctx.rect(margin, currentY, widget_width - margin * 2, H); - ctx.fill(); - if (show_text) { - if (!this.disabled) - ctx.stroke(); - ctx.fillStyle = text_color; - if (!this.disabled) { - ctx.beginPath(); - ctx.moveTo(margin + 16, currentY + 5); - ctx.lineTo(margin + 6, currentY + H * 0.5); - ctx.lineTo(margin + 16, currentY + H - 5); - ctx.fill(); - ctx.beginPath(); - ctx.moveTo(widget_width - margin - 16, currentY + 5); - ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5); - ctx.lineTo(widget_width - margin - 16, currentY + H - 5); - ctx.fill(); - } - ctx.fillStyle = secondary_text_color; - ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7); - ctx.fillStyle = text_color; - ctx.textAlign = "right"; - - ctx.fillText( - Number(this.value).toFixed( - this.options?.precision !== undefined - ? this.options.precision - : 3 - ), - widget_width - margin * 2 - 20, - currentY + H * 0.7 - ); - } - } - }, - mouse: function (event, pos, node) { - var old_value = this.value; - var x = pos[0] - node.pos[0]; - var y = pos[1] - node.pos[1]; - var width = node.size[0]; - var H = LiteGraph.NODE_WIDGET_HEIGHT; - var margin = 5; - var numWidgets = 4; // Number of stacked widgets - - for (let i = 0; i < numWidgets; i++) { - let currentY = y + i * (H + margin); // Adjust Y position for each widget - - - if (event.type == LiteGraph.pointerevents_method + "move" && this.type == "BBOX") { - if (event.deltaX) - this.value += event.deltaX * 0.1 * (this.options?.step || 1); - if (this.options.min != null && this.value < this.options.min) { - this.value = this.options.min; - } - if (this.options.max != null && this.value > this.options.max) { - this.value = this.options.max; - } - } else if (event.type == LiteGraph.pointerevents_method + "down") { - var values = this.options?.values; - if (values && values.constructor === Function) { - values = this.options.values(w, node); - } - var values_list = null; - - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (this.type == "BBOX") { - this.value += delta * 0.1 * (this.options.step || 1); - if (this.options.min != null && this.value < this.options.min) { - this.value = this.options.min; - } - if (this.options.max != null && this.value > this.options.max) { - this.value = this.options.max; - } - } else if (delta) { //clicked in arrow, used for combos - var index = -1; - this.last_mouseclick = 0; //avoids dobl click event - if (values.constructor === Object) - index = values_list.indexOf(String(this.value)) + delta; - else - index = values_list.indexOf(this.value) + delta; - if (index >= values_list.length) { - index = values_list.length - 1; - } - if (index < 0) { - index = 0; - } - if (values.constructor === Array) - this.value = values[index]; - else - this.value = index; - } - } //end mousedown - else if (event.type == LiteGraph.pointerevents_method + "up" && this.type == "BBOX") { - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (event.click_time < 200 && delta == 0) { - this.prompt("Value", this.value, function (v) { - // check if v is a valid equation or a number - if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { - try {//solve the equation if possible - v = eval(v); - } catch (e) { } - } - this.value = Number(v); - shared.inner_value_change(this, this.value, event); - }.bind(w), - event); - } - } - - if (old_value != this.value) - setTimeout( - function () { - shared.inner_value_change(this, this.value, event); - }.bind(this), - 20 - ); - - app.canvas.setDirty(true); - } - - }, - computeSize: function (width) { - return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4]; - }, - // onDrawBackground: function (ctx) { - // if (!this.flags.collapsed) return; - // this.inputEl.style.display = "block"; - // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; - // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; - // }, - // onInputChange: function (e) { - // const property = e.target.dataset.property; - // const bbox = this.getInputData(0); - // if (!bbox) return; - // bbox[property] = parseFloat(e.target.value); - // this.setOutputData(0, bbox); - // } - } - - widget.desc = "Represents a Bounding Box with x, y, width, and height."; - return widget - - }, - BOOL: (key, val, compute = false) => { - /** @type {import("/types/litegraph").IWidget} */ - const widget = { - name: key, - type: "BOOL", - options: { default: false }, - y: 0, - value: val || false, - draw: function (ctx, - node, - widget_width, - widgetY, - height) { - const hide = this.type !== "BOOL" && app.canvas.ds.scale > 0.5 - if (hide) { - return - } - const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; - const background_color = LiteGraph.WIDGET_BGCOLOR; - const text_color = LiteGraph.WIDGET_TEXT_COLOR; - const H = LiteGraph.NODE_WIDGET_HEIGHT; - const arrowSize = 8; - - var margin = 15; - if (hide) return; - - var currentY = widgetY; - - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - // ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]); - ctx.rect(margin, currentY, H, H); // Draw checkbox square - - ctx.fill(); - ctx.stroke(); - - ctx.fillStyle = text_color; - // ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7); - ctx.fillText(this.label || this.name, H + margin * 2, currentY + H * 0.7); - - - // Draw arrow if the value is true - // Draw checkmark if the value is true - if (this.value) { - ctx.fillStyle = text_color; - ctx.beginPath(); - ctx.moveTo(margin + H * 0.15, currentY + H * 0.5); - ctx.lineTo(margin + H * 0.4, currentY + H * 0.8); - ctx.lineTo(margin + H * 0.85, currentY + H * 0.2); - ctx.stroke(); - } - - }, - get value() { - - return this.inputEl.value === "true"; - }, - set value(x) { - this.inputEl.value = x; - }, - computeSize: function (width) { - return [width, 32]; - }, - mouse: function (event, pos, node) { - // var x = pos[0] - node.pos[0]; - // var y = pos[1] - node.pos[1]; - // var width = node.size[0]; - // var H = LiteGraph.NODE_WIDGET_HEIGHT; - // var margin = 15; - - // if (event.type == LiteGraph.pointerevents_method + "down") { - // if (x > margin && x < widget_width - margin && y > widgetY && y < widgetY + H) { - // this.value = !this.value; // Toggle checkbox value - // shared.inner_value_change(this, this.value, event); - // app.canvas.setDirty(true); - // } - // } - if (event.type === "pointerdown") { - // get widgets of type type : "COLOR" - const widgets = node.widgets.filter(w => w.type === "BOOL"); - - for (const w of widgets) { - // color picker - const rect = [w.last_y, w.last_y + 32]; - if (pos[1] > rect[0] && pos[1] < rect[1]) { - // picker.style.position = "absolute"; - // picker.style.left = ( pos[0]) + "px"; - // picker.style.top = ( pos[1]) + "px"; - - // place at screen center - // picker.style.position = "absolute"; - // picker.style.left = (window.innerWidth / 2) + "px"; - // picker.style.top = (window.innerHeight / 2) + "px"; - // picker.style.transform = "translate(-50%, -50%)"; - // picker.style.zIndex = 1000; - console.log("Clicked a BOOL", this.value) - - this.value = this.value ? "false" : "true" - - } - } - } - } - } - - // create a checkbox - widget.inputEl = document.createElement("input") - widget.inputEl.type = "checkbox" - widget.inputEl.value = false - document.body.appendChild(widget.inputEl); - return widget - - }, - COLOR: (key, val, compute = false) => { - /** @type {import("/types/litegraph").IWidget} */ - const widget = {} - widget.y = 0 - widget.name = key; - widget.type = "COLOR"; - widget.options = { default: "#ff0000" }; - widget.value = val || "#ff0000"; - widget.draw = function (ctx, - node, - widgetWidth, - widgetY, - height) { - const hide = this.type !== "COLOR" && app.canvas.ds.scale > 0.5 - if (hide) { - return - } - - const border = 3; - // draw a rect with a border and a fill color - ctx.fillStyle = "#000"; - ctx.fillRect(0, widgetY, widgetWidth, height); - ctx.fillStyle = this.value; - ctx.fillRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); - // write the input name - // choose the fill based on the luminoisty of this.value color - const color = parseCss(this.value.default || this.value) - if (!color) { - return - } - ctx.fillStyle = shared.isColorBright(color.values, 125) ? "#000" : "#fff"; - - - ctx.font = "14px Arial"; - ctx.textAlign = "center"; - ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14); - - - - // ctx.strokeStyle = "#fff"; - // ctx.strokeRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); - - - // ctx.fillStyle = "#000"; - // ctx.fillRect(widgetWidth/2 - border / 2 , widgetY + border / 2 , widgetWidth/2 + border / 2, height + border / 2); - // ctx.fillStyle = this.value; - // ctx.fillRect(widgetWidth/2, widgetY, widgetWidth/2, height); - - } - widget.mouse = function (e, pos, node) { - if (e.type === "pointerdown") { - // get widgets of type type : "COLOR" - const widgets = node.widgets.filter(w => w.type === "COLOR"); - - for (const w of widgets) { - // color picker - const rect = [w.last_y, w.last_y + 32]; - if (pos[1] > rect[0] && pos[1] < rect[1]) { - console.log("color picker", node) - const picker = document.createElement("input"); - picker.type = "color"; - picker.value = this.value; - // picker.style.position = "absolute"; - // picker.style.left = ( pos[0]) + "px"; - // picker.style.top = ( pos[1]) + "px"; - - // place at screen center - picker.style.position = "absolute"; - picker.style.left = "999999px"//(window.innerWidth / 2) + "px"; - picker.style.top = "999999px" //(window.innerHeight / 2) + "px"; - // picker.style.transform = "translate(-50%, -50%)"; - // picker.style.zIndex = 1000; - - - - document.body.appendChild(picker); - - picker.addEventListener("change", () => { - this.value = picker.value; - node.graph._version++; - node.setDirtyCanvas(true, true); - picker.remove(); - }); - - picker.click() - - } - } - } - } - widget.computeSize = function (width) { - return [width, 32]; - } - - return widget; - }, - - DEBUG_IMG: (val, index) => { - const w = { - name: `anything_${index}`, - type: "image", - value: val, - draw: function (ctx, - node, - widgetWidth, - widgetY, - height) { - const [cw, ch] = this.computeSize(widgetWidth) - shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) - }, - computeSize: function (width) { - const ratio = this.inputRatio || 1; - if (width) { - return [width, width / ratio + 4] - } - return [128, 128] - }, - onRemove: function () { - if (this.inputEl) { - this.inputEl.remove(); - } - } - } - - w.inputEl = document.createElement("img"); - w.inputEl.src = w.value; - w.inputEl.onload = function () { - w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight; - } - document.body.appendChild(w.inputEl); - return w - }, - DEBUG_STRING: (val, index) => { - const w = { - name: `anything_${index}`, - type: "debug_text", - val: val, - draw: function (ctx, - node, - widgetWidth, - widgetY, - height) { - // const [cw, ch] = this.computeSize(widgetWidth) - shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) - }, - computeSize: function (width) { - const value = this.inputEl.innerHTML - if (!value) { - return [32, 32] - } - if (!width) { - log(`No width ${this.parent.size}`) - } - - const fontSize = 25; // Assuming 1rem = 16px - - const oldFont = app.ctx.font - app.ctx.font = `${fontSize}px Arial`; - - const words = value.split(" "); - const lines = []; - let currentLine = ""; - for (const word of words) { - const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}`; - - const testWidth = app.ctx.measureText(testLine).width; - - // log(`Testing line ${testLine}, width: ${testWidth}, width: ${width}, ratio: ${testWidth / width}`) - if (testWidth > width) { - lines.push(currentLine); - currentLine = word; - } else { - currentLine = testLine; - } - } - app.ctx.font = oldFont; - lines.push(currentLine); - - // Step 3: Calculate the widget width and height - const textHeight = lines.length * (fontSize + 2); // You can adjust the line height (2 in this case) - const maxLineWidth = lines.reduce((maxWidth, line) => Math.max(maxWidth, app.ctx.measureText(line).width), 0); - const widgetWidth = Math.max(width || this.width || 32, maxLineWidth); - const widgetHeight = textHeight + 10; // Additional padding for spacing - return [widgetWidth, widgetHeight + 4] - - }, - onRemove: function () { - if (this.inputEl) { - this.inputEl.remove(); - } - - } - } - w.inputEl = document.createElement("p"); - w.inputEl.style.textAlign = "center"; - w.inputEl.style.fontSize = "1.5em"; - w.inputEl.style.color = "var(--input-text)"; - w.inputEl.style.fontFamily = "monospace"; - w.inputEl.innerHTML = val - document.body.appendChild(w.inputEl); - - return w - } -} - - - -const bboxWidgetDOM = (key, val) => { + BBOX: (key, val) => { /** @type {import("./types/litegraph").IWidget} */ const widget = { - name: key, - type: "BBOX", - // options: val, - y: 0, - value: val || [0, 0, 0, 0], + name: key, + type: 'BBOX', + // options: val, + y: 0, + value: val?.default || [0, 0, 0, 0], + options: {}, - draw: function (ctx, - node, - widgetWidth, - widgetY, - height) { - const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5 - this.inputEl.style.display = hide ? "none" : "block"; - if (hide) return; + draw: function (ctx, node, widget_width, widgetY, height) { + const hide = this.type !== 'BBOX' && app.canvas.ds.scale > 0.5 - shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) - }, - computeSize: function (width) { - return [width, 32]; - }, - // onDrawBackground: function (ctx) { - // if (!this.flags.collapsed) return; - // this.inputEl.style.display = "block"; - // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; - // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; - // }, - onInputChange: function (e) { - const property = e.target.dataset.property; - const bbox = this.getInputData(0); - if (!bbox) return; - bbox[property] = parseFloat(e.target.value); - this.setOutputData(0, bbox); + const show_text = true + const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR + const background_color = LiteGraph.WIDGET_BGCOLOR + const text_color = LiteGraph.WIDGET_TEXT_COLOR + const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR + const H = LiteGraph.NODE_WIDGET_HEIGHT + + let margin = 15 + let numWidgets = 4 // Number of stacked widgets + + if (hide) return + + for (let i = 0; i < numWidgets; i++) { + let currentY = widgetY + i * (H + margin) // Adjust Y position for each widget + + ctx.textAlign = 'left' + ctx.strokeStyle = outline_color + ctx.fillStyle = background_color + ctx.beginPath() + if (show_text) + ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [ + H * 0.5, + ]) + else ctx.rect(margin, currentY, widget_width - margin * 2, H) + ctx.fill() + if (show_text) { + if (!this.disabled) ctx.stroke() + ctx.fillStyle = text_color + if (!this.disabled) { + ctx.beginPath() + ctx.moveTo(margin + 16, currentY + 5) + ctx.lineTo(margin + 6, currentY + H * 0.5) + ctx.lineTo(margin + 16, currentY + H - 5) + ctx.fill() + ctx.beginPath() + ctx.moveTo(widget_width - margin - 16, currentY + 5) + ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5) + ctx.lineTo(widget_width - margin - 16, currentY + H - 5) + ctx.fill() + } + ctx.fillStyle = secondary_text_color + ctx.fillText( + this.label || this.name, + margin * 2 + 5, + currentY + H * 0.7 + ) + ctx.fillStyle = text_color + ctx.textAlign = 'right' + + ctx.fillText( + Number(this.value).toFixed( + this.options?.precision !== undefined + ? this.options.precision + : 3 + ), + widget_width - margin * 2 - 20, + currentY + H * 0.7 + ) + } } + }, + mouse: function (event, pos, node) { + let old_value = this.value + let x = pos[0] - node.pos[0] + let y = pos[1] - node.pos[1] + let width = node.size[0] + let H = LiteGraph.NODE_WIDGET_HEIGHT + let margin = 5 + let numWidgets = 4 // Number of stacked widgets + + for (let i = 0; i < numWidgets; i++) { + let currentY = y + i * (H + margin) // Adjust Y position for each widget + + if ( + event.type == LiteGraph.pointerevents_method + 'move' && + this.type == 'BBOX' + ) { + if (event.deltaX) + this.value += event.deltaX * 0.1 * (this.options?.step || 1) + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min + } + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max + } + } else if (event.type == LiteGraph.pointerevents_method + 'down') { + let values = this.options?.values + if (values && values.constructor === Function) { + values = this.options.values(w, node) + } + let values_list = null + + let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 + if (this.type == 'BBOX') { + this.value += delta * 0.1 * (this.options.step || 1) + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min + } + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max + } + } else if (delta) { + //clicked in arrow, used for combos + let index = -1 + this.last_mouseclick = 0 //avoids dobl click event + if (values.constructor === Object) + index = values_list.indexOf(String(this.value)) + delta + else index = values_list.indexOf(this.value) + delta + if (index >= values_list.length) { + index = values_list.length - 1 + } + if (index < 0) { + index = 0 + } + if (values.constructor === Array) this.value = values[index] + else this.value = index + } + } //end mousedown + else if ( + event.type == LiteGraph.pointerevents_method + 'up' && + this.type == 'BBOX' + ) { + let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 + if (event.click_time < 200 && delta == 0) { + this.prompt( + 'Value', + this.value, + function (v) { + // check if v is a valid equation or a number + if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { + try { + //solve the equation if possible + v = eval(v) + } catch (e) {} + } + this.value = Number(v) + shared.inner_value_change(this, this.value, event) + }.bind(w), + event + ) + } + } + + if (old_value != this.value) + setTimeout( + function () { + shared.inner_value_change(this, this.value, event) + }.bind(this), + 20 + ) + + app.canvas.setDirty(true) + } + }, + computeSize: function (width) { + return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4] + }, + // onDrawBackground: function (ctx) { + // if (!this.flags.collapsed) return; + // this.inputEl.style.display = "block"; + // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; + // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; + // }, + // onInputChange: function (e) { + // const property = e.target.dataset.property; + // const bbox = this.getInputData(0); + // if (!bbox) return; + // bbox[property] = parseFloat(e.target.value); + // this.setOutputData(0, bbox); + // } } - widget.inputEl = document.createElement("div") - widget.parent = this - widget.inputEl.innerHTML = ` -
-
-
-
- `; - // set the class document wide if not present - shared.defineClass("bbox-input", `background-color: var(--comfy-input-bg); - color: var(--input-text); - overflow: hidden; - width:100%; - overflow-y: auto; - padding: 2px; - resize: none; - border: none; - box-sizing: border-box; - font-size: 10px;`) - - - const bboxInputs = widget.inputEl.querySelectorAll(".bbox-input"); - bboxInputs.forEach((input) => { - input.addEventListener("change", widget.onInputChange.bind(this)); - }); - - widget.desc = "Represents a Bounding Box with x, y, width, and height."; - - document.body.appendChild(widget.inputEl); - - - console.log("Bounding Box Widget DOM", widget.inputEl) + widget.desc = 'Represents a Bounding Box with x, y, width, and height.' return widget + }, + BOOL: (key, val, compute = false) => { + /** @type {import("/types/litegraph").IWidget} */ + const widget = { + name: key, + type: 'BOOL', + options: { default: false }, + y: 0, + draw: function (ctx, node, widget_width, widgetY, height) { + const hide = this.type !== 'BOOL' && app.canvas.ds.scale > 0.5 + if (hide) { + return + } + const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR + const background_color = LiteGraph.WIDGET_BGCOLOR + const text_color = LiteGraph.WIDGET_TEXT_COLOR + const H = LiteGraph.NODE_WIDGET_HEIGHT + // const arrowSize = 8 + + let margin = 15 + if (hide) return + + let currentY = widgetY + + ctx.textAlign = 'left' + ctx.strokeStyle = outline_color + ctx.fillStyle = background_color + ctx.beginPath() + // ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]); + ctx.rect(margin, currentY, H, H) // Draw checkbox square + + ctx.fill() + ctx.stroke() + + ctx.fillStyle = text_color + // ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7); + ctx.fillText( + this.label || this.name, + H + margin * 2, + currentY + H * 0.7 + ) + + // Draw arrow if the value is true + // Draw checkmark if the value is true + if (this.value) { + ctx.fillStyle = text_color + ctx.beginPath() + ctx.moveTo(margin + H * 0.15, currentY + H * 0.5) + ctx.lineTo(margin + H * 0.4, currentY + H * 0.8) + ctx.lineTo(margin + H * 0.85, currentY + H * 0.2) + ctx.stroke() + } + }, + get value() { + return this.inputEl.value === 'true' + }, + set value(x) { + this.inputEl.value = x + }, + computeSize: function (width) { + return [width, 32] + }, + mouse: function (event, pos, node) { + // let x = pos[0] - node.pos[0]; + // let y = pos[1] - node.pos[1]; + // let width = node.size[0]; + // let H = LiteGraph.NODE_WIDGET_HEIGHT; + // let margin = 15; + + // if (event.type == LiteGraph.pointerevents_method + "down") { + // if (x > margin && x < widget_width - margin && y > widgetY && y < widgetY + H) { + // this.value = !this.value; // Toggle checkbox value + // shared.inner_value_change(this, this.value, event); + // app.canvas.setDirty(true); + // } + // } + if (event.type === 'pointerdown') { + // get widgets of type type : "COLOR" + const widgets = node.widgets.filter((w) => w.type === 'BOOL') + + for (const w of widgets) { + // color picker + const rect = [w.last_y, w.last_y + 32] + if (pos[1] > rect[0] && pos[1] < rect[1]) { + // picker.style.position = "absolute"; + // picker.style.left = ( pos[0]) + "px"; + // picker.style.top = ( pos[1]) + "px"; + + // place at screen center + // picker.style.position = "absolute"; + // picker.style.left = (window.innerWidth / 2) + "px"; + // picker.style.top = (window.innerHeight / 2) + "px"; + // picker.style.transform = "translate(-50%, -50%)"; + // picker.style.zIndex = 1000; + + this.value = this.value ? false : true + } + } + } + }, + } + + // create a checkbox + widget.inputEl = document.createElement('input') + widget.inputEl.type = 'checkbox' + widget.value = val || false + + document.body.appendChild(widget.inputEl) + return widget + }, + COLOR: (key, val, compute = false) => { + /** @type {import("/types/litegraph").IWidget} */ + const widget = {} + widget.y = 0 + widget.name = key + widget.type = 'COLOR' + widget.options = { default: '#ff0000' } + widget.value = val || '#ff0000' + widget.draw = function (ctx, node, widgetWidth, widgetY, height) { + const hide = this.type !== 'COLOR' && app.canvas.ds.scale > 0.5 + if (hide) { + return + } + const border = 3 + ctx.fillStyle = '#000' + ctx.fillRect(0, widgetY, widgetWidth, height) + ctx.fillStyle = this.value + ctx.fillRect( + border, + widgetY + border, + widgetWidth - border * 2, + height - border * 2 + ) + const color = parseCss(this.value.default || this.value) + if (!color) { + return + } + ctx.fillStyle = shared.isColorBright(color.values, 125) ? '#000' : '#fff' + + ctx.font = '14px Arial' + ctx.textAlign = 'center' + ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14) + } + widget.mouse = function (e, pos, node) { + if (e.type === 'pointerdown') { + const widgets = node.widgets.filter((w) => w.type === 'COLOR') + + for (const w of widgets) { + // color picker + const rect = [w.last_y, w.last_y + 32] + if (pos[1] > rect[0] && pos[1] < rect[1]) { + const picker = document.createElement('input') + picker.type = 'color' + picker.value = this.value + + picker.style.position = 'absolute' + picker.style.left = '999999px' //(window.innerWidth / 2) + "px"; + picker.style.top = '999999px' //(window.innerHeight / 2) + "px"; + + document.body.appendChild(picker) + + picker.addEventListener('change', () => { + this.value = picker.value + node.graph._version++ + node.setDirtyCanvas(true, true) + picker.remove() + }) + + picker.click() + } + } + } + } + widget.computeSize = function (width) { + return [width, 32] + } + + return widget + }, + + DEBUG_IMG: (name, val) => { + const w = { + name, + type: 'image', + value: val, + draw: function (ctx, node, widgetWidth, widgetY, height) { + const [cw, ch] = this.computeSize(widgetWidth) + shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) + }, + computeSize: function (width) { + const ratio = this.inputRatio || 1 + if (width) { + return [width, width / ratio + 4] + } + return [128, 128] + }, + onRemoved: function () { + if (this.inputEl) { + this.inputEl.remove() + } + }, + } + + w.inputEl = document.createElement('img') + w.inputEl.src = w.value + w.inputEl.onload = function () { + w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight + } + document.body.appendChild(w.inputEl) + return w + }, + DEBUG_STRING: (name, val) => { + const fontSize = 16 + const w = { + name, + type: 'debug_text', + + draw: function (ctx, node, widgetWidth, widgetY, height) { + // const [cw, ch] = this.computeSize(widgetWidth) + shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) + }, + computeSize: function (width) { + const value = this.inputEl.innerHTML + if (!value) { + return [32, 32] + } + if (!width) { + log(`No width ${this.parent.size}`) + } + + const oldFont = app.ctx.font + app.ctx.font = `${fontSize}px monospace` + + const words = value.split(' ') + const lines = [] + let currentLine = '' + for (const word of words) { + const testLine = + currentLine.length === 0 ? word : `${currentLine} ${word}` + + const testWidth = app.ctx.measureText(testLine).width + + if (testWidth > width) { + lines.push(currentLine) + currentLine = word + } else { + currentLine = testLine + } + } + app.ctx.font = oldFont + if (lines.length === 0) lines.push(currentLine) + + const textHeight = (lines.length + 1) * fontSize + + const maxLineWidth = lines.reduce( + (maxWidth, line) => + Math.max(maxWidth, app.ctx.measureText(line).width), + 0 + ) + const widgetWidth = Math.max(width || this.width || 32, maxLineWidth) + const widgetHeight = textHeight * 1.5 + return [widgetWidth, widgetHeight] + }, + onRemoved: function () { + if (this.inputEl) { + this.inputEl.remove() + } + }, + } + + Object.defineProperty(w, 'value', { + get() { + return this.inputEl.innerHTML + }, + set(value) { + this.inputEl.innerHTML = value + this.parent?.setSize?.(this.parent?.computeSize()) + }, + }) + + w.inputEl = document.createElement('p') + w.inputEl.style.textAlign = 'center' + w.inputEl.style.fontSize = `${fontSize}px` + w.inputEl.style.color = 'var(--input-text)' + w.inputEl.style.lineHeight = 0 + + w.inputEl.style.fontFamily = 'monospace' + w.value = val + document.body.appendChild(w.inputEl) + + return w + }, } -/** - * @returns {import("./types/litegraph").IWidget} widget - */ - - -/** - * @returns {import("./types/litegraph").IWidget} widget - */ - - - -// VIDEO: (node, inputName, inputData, app) => { -// console.log("video") -// const videoWidget = { -// name: "VideoWidget", -// description: "Video Player Widget", -// value: inputData, -// properties: {}, -// widget: null, - -// init: function () { -// this.widget = document.createElement("video"); -// this.widget.width = 200; -// this.widget.height = 120; -// this.widget.controls = true; -// this.widget.style.width = "100%"; -// this.widget.style.height = "100%"; -// this.widget.style.objectFit = "contain"; -// this.widget.style.backgroundColor = "black"; -// this.widget.style.pointerEvents = "none"; -// node.addWidget(inputName, videoWidget.widget, inputData); -// }, - -// setValue: function (value, options) { -// if (value instanceof HTMLVideoElement) { -// this.widget.src = value.src; -// } else if (typeof value === "string") { -// this.widget.src = value; -// } -// }, - -// getValue: function () { -// return this.widget.src; -// }, - -// append: function (parent) { -// parent.appendChild(this.widget); -// }, - -// remove: function () { -// this.widget.parentNode.removeChild(this.widget); -// } -// }; -// return { -// widget: videoWidget, -// } -// } - - /** * @returns {import("./types/comfy").ComfyExtension} extension */ const mtb_widgets = { - name: "mtb.widgets", - - init: async () => { - console.log("Registering mtb.widgets") - try { - - const res = await api.fetchApi('/mtb/debug') - const msg = await res.json() - window.MTB_DEBUG = msg.enabled; - } - catch (e) { - console.error('Error:', error); - } - }, - - setup: () => { - app.ui.settings.addSetting({ - id: "mtb.Debug.enabled", - name: "[mtb] Enable Debug (py and js)", - type: "boolean", - defaultValue: false, - - tooltip: - "This will enable debug messages in the console and in the python console respectively", - attrs: { - style: { - fontFamily: "monospace", - }, - }, - async onChange(value) { - if (value) { - console.log("Enabled DEBUG mode") - } - window.MTB_DEBUG = value; - await api.fetchApi('/mtb/debug', { - method: 'POST', - body: JSON.stringify({ - enabled: value - - }) - }).then(response => { }).catch(error => { - console.error('Error:', error); - }); - - }, - }); - }, - - - getCustomWidgets: function () { - return { - BOOL: (node, inputName, inputData, app) => { - console.debug("Registering bool") - - return { - widget: node.addCustomWidget(MtbWidgets.BOOL(inputName, inputData[1]?.default || false)), - minWidth: 150, - minHeight: 30, - }; - }, - - COLOR: (node, inputName, inputData, app) => { - console.debug("Registering color") - return { - widget: node.addCustomWidget(MtbWidgets.COLOR(inputName, inputData[1]?.default || "#ff0000")), - minWidth: 150, - minHeight: 30, - } - }, - // BBOX: (node, inputName, inputData, app) => { - // console.debug("Registering bbox") - // return { - // widget: node.addCustomWidget(MtbWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])), - // minWidth: 150, - // minHeight: 30, - // } - - // } - } - }, - /** - * @param {import("./types/comfy").NodeType} nodeType - * @param {import("./types/comfy").NodeDef} nodeData - * @param {import("./types/comfy").App} app - */ - async beforeRegisterNodeDef(nodeType, nodeData, app) { - - const rinputs = nodeData.input?.required; - - let has_custom = false - if (nodeData.input && nodeData.input.required) { - for (const i of Object.keys(nodeData.input.required)) { - const input_type = nodeData.input.required[i][0] - - if (newTypes.includes(input_type)) { - has_custom = true - break; - } - } - } - if (has_custom) { - - //- Add widgets on node creation - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; - this.serialize_widgets = true; - for (const [key, input] of Object.entries(rinputs)) { - switch (input[0]) { - case "COLOR": - // const colW = colorWidget(key, input[1]) - // this.addCustomWidget(colW) - // const associated_input = this.inputs.findIndex((i) => i.widget?.name === key); - // if (associated_input !== -1) { - // this.inputs[associated_input].widget = colW - // } - - - - break; - case "BOOL": - // const widg = boolWidget(key, input[1]) - // this.addCustomWidget(widg) - // this.addWidget("toggle", key, false, function (value, widget, node) { - // console.log(value) - - // }) - //this.removeInput(this.inputs.findIndex((i) => i.widget?.name === key)); - - break; - case "BBOX": - // const bboxW = bboxWidget(key, input[1]) - // this.addCustomWidget(bboxW) - break; - default: - break - } - - - // } - } - - this.setSize?.(this.computeSize()) - - this.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].canvas) { - this.widgets[y].canvas.remove(); - } - } - }; - } - - //- Extra menus - const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; - nodeType.prototype.getExtraMenuOptions = function (_, options) { - const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : undefined; - if (this.widgets) { - let toInput = []; - let toWidget = []; - for (const w of this.widgets) { - if (w.type === shared.CONVERTED_TYPE) { - //- This is already handled by widgetinputs.js - // toWidget.push({ - // content: `Convert ${w.name} to widget`, - // callback: () => shared.convertToWidget(this, w), - // }); - } else if (newTypes.includes(w.type)) { - const config = nodeData?.input?.required[w.name] || nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}]; - - toInput.push({ - content: `Convert ${w.name} to input`, - callback: () => shared.convertToInput(this, w, config), - }); - } - } - if (toInput.length) { - options.push(...toInput, null); - } - - if (toWidget.length) { - options.push(...toWidget, null); - } - } - - return r; - }; - - } - - //- Extending Python Nodes - switch (nodeData.name) { - case "Psd Save (mtb)": { - // const onConnectionsChange = nodeType.prototype.onConnectionsChange; - nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) { - // const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined; - shared.dynamic_connection(this, index, connected) - } - break - } - case "Save Gif (mtb)": { - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (message) { - const r = onExecuted ? onExecuted.apply(this, message) : undefined; - console.log(message) - if (this.widgets) { - const pos = this.widgets.findIndex((w) => w.name === "anything_0"); - if (pos !== -1) { - for (let i = pos; i < this.widgets.length; i++) { - console.log(this.widgets[i]) - console.log(this) - this.widgets[i].onRemove?.(); - - } - this.widgets.length = pos - - - } - - let imgURLs = [] - if (message && message.gif) { - imgURLs = imgURLs.concat(message.gif.map(params => { - return api.apiURL("/view?" + new URLSearchParams(params).toString()); - })) - console.log(imgURLs) - for (const img of imgURLs) { - const w = this.addCustomWidget(MtbWidgets.DEBUG_IMG(img, 0)) - w.parent = this; - } - } - this.setSize?.(this.computeSize()) - return r - - } - - const onRemoved = nodeType.prototype.onRemoved; - nodeType.prototype.onRemoved = function (message) { - const r = onRemoved ? onRemoved.apply(this, message) : undefined; - if (!this.widgets) return r - for (const w of this.widgets) { - if (w.canvas) { - w.canvas.remove(); - } - w.onRemove?.() - w.onRemoved?.() - } - return r - - } - } - - break - } - case "Animation Builder (mtb)": { - // console.log(nodeType.prototype) - - - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; - - this.changeMode(LiteGraph.ALWAYS) - // api.addEventListener("executed", ({ detail }) => { - - // console.log("executed", detail) - // console.log(this) - - // }) - const raw_iteration = this.widgets.find((w) => w.name === "raw_iteration"); - const raw_loop = this.widgets.find((w) => w.name === "raw_loop"); - - - const total_frames = this.widgets.find((w) => w.name === "total_frames"); - const loop_count = this.widgets.find((w) => w.name === "loop_count"); - - shared.hideWidgetForGood(this, raw_iteration); - shared.hideWidgetForGood(this, raw_loop); - - raw_iteration._value = 0 - // Object.defineProperty(raw_iteration, "value", { - // get() { - // return this._value - // }, - // set(value) { - // this._value = value; - // }, - // }); - - const value_preview = ComfyWidgets["STRING"](this, "PREVIEW_raw_iteration", ["STRING", { multiline: true }], app).widget; - value_preview.inputEl.readOnly = true; - value_preview.inputEl.disabled = true; - - - // value_preview.inputEl.style.opacity = 0.6; - value_preview.inputEl.style.textAlign = "center"; - value_preview.inputEl.style.fontSize = "2.5em"; - value_preview.inputEl.style.backgroundColor = "black"; - - value_preview.inputEl.style.setProperty("--comfy-input-bg", "transparent"); - value_preview.inputEl.style.setProperty("background", "red", "important"); - // remove the comfy-multiline-input class - - // disallow user selection - value_preview.inputEl.style.userSelect = "none"; - - const loop_preview = ComfyWidgets["STRING"](this, "PREVIEW_raw_iteration", ["STRING", { multiline: true }], app).widget; - loop_preview.inputEl.readOnly = true; - loop_preview.inputEl.disabled = true; - - - // loop_preview.inputEl.style.opacity = 0.6; - loop_preview.inputEl.style.textAlign = "center"; - loop_preview.inputEl.style.fontSize = "1.5em"; - loop_preview.inputEl.style.backgroundColor = "black"; - - loop_preview.inputEl.style.setProperty("--comfy-input-bg", "transparent"); - loop_preview.inputEl.style.setProperty("background", "red", "important"); - // remove the comfy-multiline-input class - - // disallow user selection - loop_preview.inputEl.style.userSelect = "none"; - - const onReset = () => { - raw_iteration.value = 0; - raw_loop.value = 0; - - value_preview.value = 0; - loop_preview.value = 0; - - app.canvas.setDirty(true); - } - - const reset_button = this.addWidget("button", `Reset`, "reset", onReset); - - const run_button = this.addWidget("button", `Queue`, "queue", () => { - onReset() // this could maybe be a setting or checkbox - app.queuePrompt(0, total_frames.value * loop_count.value) - - }); - - - - raw_iteration.afterQueued = function () { - this.value++; - raw_loop.value = Math.floor(this.value / total_frames.value); - value_preview.value = `raw: ${this.value} -frame: ${this.value % total_frames.value}`; - if (raw_loop.value + 1 > loop_count.value) { - loop_preview.value = `Done 😎!` - return - } - - loop_preview.value = `current loop: ${raw_loop.value + 1}/${loop_count.value}` - - } - - return r - - } - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (data) { - onExecuted?.apply(this, data) - if (this.widgets) { - const pos = this.widgets.findIndex((w) => w.name === "preview"); - if (pos !== -1) { - for (let i = pos; i < this.widgets.length; i++) { - this.widgets[i].onRemove?.(); - } - this.widgets.length = pos; - } - } - - const w = ComfyWidgets["STRING"](this, "preview", ["STRING", { multiline: true }], app).widget; - w.inputEl.readOnly = true; - w.inputEl.style.opacity = 0.6; - w.value = data.total_frames; - - // this.onResize?.(this.size); - this.setSize?.(this.computeSize()) - - } - // const onAfterExecuteNode = nodeType.prototype.onAfterExecuteNode; - // nodeType.prototype.onAfterExecuteNode = function () { - // onAfterExecuteNode?.apply(this) - // console.log("after", this) - - // } - console.debug(`Registered ${nodeType.name} node extra events`) - break - - } - default: { - break - - } - - } - // const onNodeCreated = nodeType.prototype.onNodeCreated; - - // nodeType.prototype.onNodeCreated = function () { - // const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; - - // } - - - - - // console.log(nodeData.output) - // if (nodeData.output.includes("VIDEO") && nodeData.output_node) { - // console.log(`Found video output for ${nodeType}`) - // console.log(nodeData) - - // } - - // if (nodeData.name === "Psd Save (mtb)") { - // console.log(`Found psd node`) - // console.log(nodeData) - - // } - - - + name: 'mtb.widgets', + + init: async () => { + log('Registering mtb.widgets') + try { + const res = await api.fetchApi('/mtb/debug') + const msg = await res.json() + if (!window.MTB) { + window.MTB = {} + } + window.MTB.DEBUG = msg.enabled + } catch (e) { + console.error('Error:', error) } -}; + }, + setup: () => { + app.ui.settings.addSetting({ + id: 'mtb.Debug.enabled', + name: '[mtb] Enable Debug (py and js)', + type: 'boolean', + defaultValue: false, -app.registerExtension(mtb_widgets); \ No newline at end of file + tooltip: + 'This will enable debug messages in the console and in the python console respectively', + attrs: { + style: { + fontFamily: 'monospace', + }, + }, + async onChange(value) { + if (value) { + console.log('Enabled DEBUG mode') + } + if (!window.MTB) { + window.MTB = {} + } + window.MTB.DEBUG = value + await api + .fetchApi('/mtb/debug', { + method: 'POST', + body: JSON.stringify({ + enabled: value, + }), + }) + .then((response) => {}) + .catch((error) => { + console.error('Error:', error) + }) + }, + }) + }, + + getCustomWidgets: function () { + return { + BOOL: (node, inputName, inputData, app) => { + console.debug('Registering bool') + + return { + widget: node.addCustomWidget( + MtbWidgets.BOOL(inputName, inputData[1]?.default || false) + ), + minWidth: 150, + minHeight: 30, + } + }, + + COLOR: (node, inputName, inputData, app) => { + console.debug('Registering color') + return { + widget: node.addCustomWidget( + MtbWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000') + ), + minWidth: 150, + minHeight: 30, + } + }, + // BBOX: (node, inputName, inputData, app) => { + // console.debug("Registering bbox") + // return { + // widget: node.addCustomWidget(MtbWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])), + // minWidth: 150, + // minHeight: 30, + // } + + // } + } + }, + /** + * @param {import("./types/comfy").NodeType} nodeType + * @param {import("./types/comfy").NodeDef} nodeData + * @param {import("./types/comfy").App} app + */ + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // const rinputs = nodeData.input?.required + + let has_custom = false + if (nodeData.input && nodeData.input.required) { + for (const i of Object.keys(nodeData.input.required)) { + const input_type = nodeData.input.required[i][0] + + if (newTypes.includes(input_type)) { + has_custom = true + break + } + } + } + if (has_custom) { + //- Add widgets on node creation + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + this.serialize_widgets = true + this.setSize?.(this.computeSize()) + + this.onRemoved = function () { + // When removing this node we need to remove the input from the DOM + for (const w of this.widgets) { + if (w.canvas) { + w.canvas.remove() + } + w.onRemoved?.() + } + } + return r + } + + //- Extra menus + const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions + nodeType.prototype.getExtraMenuOptions = function (_, options) { + const r = origGetExtraMenuOptions + ? origGetExtraMenuOptions.apply(this, arguments) + : undefined + if (this.widgets) { + let toInput = [] + let toWidget = [] + for (const w of this.widgets) { + if (w.type === shared.CONVERTED_TYPE) { + //- This is already handled by widgetinputs.js + // toWidget.push({ + // content: `Convert ${w.name} to widget`, + // callback: () => shared.convertToWidget(this, w), + // }); + } else if (newTypes.includes(w.type)) { + const config = nodeData?.input?.required[w.name] || + nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}] + + toInput.push({ + content: `Convert ${w.name} to input`, + callback: () => shared.convertToInput(this, w, config), + }) + } + } + if (toInput.length) { + options.push(...toInput, null) + } + + if (toWidget.length) { + options.push(...toWidget, null) + } + } + + return r + } + } + + //- Extending Python Nodes + switch (nodeData.name) { + case 'Psd Save (mtb)': { + const onConnectionsChange = nodeType.prototype.onConnectionsChange + nodeType.prototype.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + const r = onConnectionsChange + ? onConnectionsChange.apply(this, arguments) + : undefined + shared.dynamic_connection(this, index, connected) + return r + } + break + } + case 'Save Gif (mtb)': { + const onExecuted = nodeType.prototype.onExecuted + nodeType.prototype.onExecuted = function (message) { + const prefix = 'anything_' + const r = onExecuted ? onExecuted.apply(this, message) : undefined + + if (this.widgets) { + const pos = this.widgets.findIndex((w) => w.name === `${prefix}_0`) + if (pos !== -1) { + for (let i = pos; i < this.widgets.length; i++) { + this.widgets[i].onRemoved?.() + } + this.widgets.length = pos + } + + let imgURLs = [] + if (message && message.gif) { + imgURLs = imgURLs.concat( + message.gif.map((params) => { + return api.apiURL( + '/view?' + new URLSearchParams(params).toString() + ) + }) + ) + + let i = 0 + for (const img of imgURLs) { + const w = this.addCustomWidget( + MtbWidgets.DEBUG_IMG(`${prefix}_${i}`, img) + ) + w.parent = this + i++ + } + } + this.setSize?.(this.computeSize()) + return r + } + + const onRemoved = nodeType.prototype.onRemoved + nodeType.prototype.onRemoved = function (message) { + const r = onRemoved ? onRemoved.apply(this, message) : undefined + if (!this.widgets) return r + for (const w of this.widgets) { + if (w.canvas) { + w.canvas.remove() + } + w.onRemoved?.() + } + return r + } + } + + break + } + case 'Animation Builder (mtb)': { + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + + this.changeMode(LiteGraph.ALWAYS) + + const raw_iteration = this.widgets.find( + (w) => w.name === 'raw_iteration' + ) + const raw_loop = this.widgets.find((w) => w.name === 'raw_loop') + + const total_frames = this.widgets.find( + (w) => w.name === 'total_frames' + ) + const loop_count = this.widgets.find((w) => w.name === 'loop_count') + + shared.hideWidgetForGood(this, raw_iteration) + shared.hideWidgetForGood(this, raw_loop) + + raw_iteration._value = 0 + + const value_preview = this.addCustomWidget( + MtbWidgets['DEBUG_STRING']('value_preview', 'Idle') + ) + value_preview.parent = this + + const loop_preview = this.addCustomWidget( + MtbWidgets['DEBUG_STRING']('loop_preview', 'Iteration: Idle') + ) + loop_preview.parent = this + + const onReset = () => { + raw_iteration.value = 0 + raw_loop.value = 0 + + value_preview.value = 'Idle' + loop_preview.value = 'Iteration: Idle' + + app.canvas.setDirty(true) + } + + const reset_button = this.addWidget( + 'button', + `Reset`, + 'reset', + onReset + ) + + const run_button = this.addWidget('button', `Queue`, 'queue', () => { + onReset() // this could maybe be a setting or checkbox + app.queuePrompt(0, total_frames.value * loop_count.value) + }) + + this.onRemoved = () => { + for (const w of this.widgets) { + if (w.canvas) { + w.canvas.remove() + } + w.onRemoved?.() + } + app.canvas.setDirty(true) + } + + raw_iteration.afterQueued = function () { + this.value++ + raw_loop.value = Math.floor(this.value / total_frames.value) + + value_preview.value = `frame: ${ + raw_iteration.value % total_frames.value + } / ${total_frames.value - 1}` + + if (raw_loop.value + 1 > loop_count.value) { + loop_preview.value = 'Done 😎!' + } else { + loop_preview.value = `current loop: ${raw_loop.value + 1}/${ + loop_count.value + }` + } + } + + return r + } + + break + } + case 'Text Encore Frames (mtb)': { + const onConnectionsChange = nodeType.prototype.onConnectionsChange + nodeType.prototype.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + const r = onConnectionsChange + ? onConnectionsChange.apply(this, arguments) + : undefined + + shared.dynamic_connection(this, index, connected) + return r + } + break + } + case 'Save Tensors (mtb)': { + const onDrawBackground = nodeType.prototype.onDrawBackground + nodeType.prototype.onDrawBackground = function (ctx, canvas) { + const r = onDrawBackground + ? onDrawBackground.apply(this, arguments) + : undefined + // // draw a circle on the top right of the node, with text inside + // ctx.fillStyle = "#fff"; + // ctx.beginPath(); + // ctx.arc(this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5, this.node_width * 0.5, 0, Math.PI * 2); + // ctx.fill(); + + // ctx.fillStyle = "#000"; + // ctx.textAlign = "center"; + // ctx.font = "bold 12px Arial"; + // ctx.fillText("Save Tensors", this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5); + + return r + } + break + } + default: { + break + } + } + }, +} + +app.registerExtension(mtb_widgets) diff --git a/web/notify.js b/web/notify.js new file mode 100644 index 0000000..60d2d73 --- /dev/null +++ b/web/notify.js @@ -0,0 +1,10 @@ +/** + * File: notify.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +import { app } from '/scripts/app.js' From 3de160af25b516c02aaa8cc32baec16e9ef358fb Mon Sep 17 00:00:00 2001 From: melMass Date: Mon, 24 Jul 2023 20:25:53 +0200 Subject: [PATCH 59/70] =?UTF-8?q?feat:=20=E2=9A=A1=EF=B8=8F=20add=20an=20"?= =?UTF-8?q?actions"=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 24 +++++++++++++++++ endpoint.py | 48 ++++++++++++++++++++++++++++++++++ web/comfy_shared.js | 9 +++++++ web/mtb_widgets.js | 63 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+) diff --git a/__init__.py b/__init__.py index 8a7e7a7..5cf2c58 100644 --- a/__init__.py +++ b/__init__.py @@ -269,6 +269,30 @@ async def get_debug(request): return web.json_response({"enabled": enabled}) +@PromptServer.instance.routes.get("/mtb/actions") +async def no_route(request): + from . import endpoint + + if "text/html" in request.headers.get("Accept", ""): + html_response = f""" +

Actions has no get for now...

+ """ + return web.Response( + text=endpoint.render_base_template("Actions", html_response), + content_type="text/html", + ) + return web.json_response({"message": "actions has no get for now"}) + + +@PromptServer.instance.routes.post("/mtb/actions") +async def do_action(request): + from . import endpoint + + reload(endpoint) + + return await endpoint.do_action(request) + + # - WAS Dictionary MANIFEST = { "name": "MTB Nodes", # The title that will be displayed on Node Class menu,. and Node Class view diff --git a/endpoint.py b/endpoint.py index a38e269..d2030e0 100644 --- a/endpoint.py +++ b/endpoint.py @@ -5,6 +5,54 @@ import os endlog = mklog("mtb endpoint") +#- ACTIONS + +def ACTIONS_getStyles(style_name=None): + from .nodes.conditions import StylesLoader + + styles = StylesLoader.options + match_list = ["name"] + if styles: + filtered_styles = { + key: value + for key, value in styles.items() + if not key.startswith("__") and key not in match_list + } + if style_name: + if style_name in filtered_styles: + return filtered_styles[style_name] + else: + return {"error": "Style not found"} + return filtered_styles + return {"error": "No styles found"} + + +async def do_action(request) -> web.Response: + endlog.debug("Init action request") + request_data = await request.json() + name = request_data.get("name") + args = request_data.get("args") + + endlog.debug(f"Received action request: {name} {args}") + + method_name = "ACTIONS_" + name + method = globals().get(method_name) + + if callable(method): + result = method(args) if args else method() + endlog.debug(f"Action result: {result}") + return web.json_response({"result": result}) + + available_methods = [ + attr[len("ACTIONS_") :] for attr in globals() if attr.startswith("ACTIONS_") + ] + + return web.json_response( + {"error": "Invalid method name.", "available_methods": available_methods} + ) + + +# - HTML UTILS def render_table(table_dict, sort=True, title=None): table_rows = "" table_dict = sorted( diff --git a/web/comfy_shared.js b/web/comfy_shared.js index f44e10c..57850ac 100644 --- a/web/comfy_shared.js +++ b/web/comfy_shared.js @@ -1,3 +1,12 @@ +/** + * File: comfy_shared.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + import { app } from '/scripts/app.js' export const log = (...args) => { diff --git a/web/mtb_widgets.js b/web/mtb_widgets.js index ab23434..11dfdb4 100644 --- a/web/mtb_widgets.js +++ b/web/mtb_widgets.js @@ -847,6 +847,69 @@ const mtb_widgets = { } break } + case 'Styles Loader (mtb)': { + const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions + nodeType.prototype.getExtraMenuOptions = function (_, options) { + const r = origGetExtraMenuOptions + ? origGetExtraMenuOptions.apply(this, arguments) + : undefined + + const getStyle = async (node) => { + try { + const getStyles = await api.fetchApi('/mtb/actions', { + method: 'POST', + body: JSON.stringify({ + name: 'getStyles', + args: + node.widgets && node.widgets[0].value + ? node.widgets[0].value + : '', + }), + }) + + const output = await getStyles.json() + return output?.result + } catch (e) { + console.error(e) + } + } + const extracters = [ + { + content: 'Extract Positive to Text node', + callback: async () => { + const style = await getStyle(this) + if (style && style.length >= 1) { + if (style[0]) { + const tn = LiteGraph.createNode('Text box') + app.graph.add(tn) + tn.title = `${this.widgets[0].value} (Positive)` + tn.widgets[0].value = style[0] + } else { + } + } + }, + }, + { + content: 'Extract Negative to Text node', + callback: async () => { + const style = await getStyle(this) + if (style && style.length >= 2) { + if (style[1]) { + const tn = LiteGraph.createNode('Text box') + app.graph.add(tn) + tn.title = `${this.widgets[0].value} (Negative)` + tn.widgets[0].value = style[1] + } else { + } + } + }, + }, + ] + options.push(...extracters) + } + + break + } case 'Save Tensors (mtb)': { const onDrawBackground = nodeType.prototype.onDrawBackground nodeType.prototype.onDrawBackground = function (ctx, canvas) { From 7e9c97ecb48672b25e5ed17b9b35dba9208ac311 Mon Sep 17 00:00:00 2001 From: melMass Date: Mon, 24 Jul 2023 20:34:07 +0200 Subject: [PATCH 60/70] =?UTF-8?q?feat:=20=E2=9C=A8=20first=20version=20of?= =?UTF-8?q?=20Notify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a very simple toast notification system that I will start to use where it makes sense. It's completely standalone and can be used by adding it to web/extensions and then calling windows.MTB.notify(), it even works in the console --- web/notify.js | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/web/notify.js b/web/notify.js index 60d2d73..fd9e2a4 100644 --- a/web/notify.js +++ b/web/notify.js @@ -8,3 +8,108 @@ */ import { app } from '/scripts/app.js' + +const log = (...args) => { + if (window.MTB?.TRACE) { + console.debug(...args) + } +} + +let transition_time = 300 + +const containerStyle = ` +position: fixed; +top: 20px; +left: 20px; +font-family: monospace; +z-index: 99999; +height: 0; +overflow: hidden; +transition: height ${transition_time}ms ease-in-out; + +` + +const toastStyle = ` + background-color: #333; + color: #fff; + padding: 10px; + border-radius: 5px; + opacity: 0; + overflow:hidden; + height:20px; + transition-property: opacity, height, padding; + transition-duration: ${transition_time}ms; + ` + +function notify(message, timeout = 3000) { + log('Creating toast') + const container = document.getElementById('mtb-notify-container') + const toast = document.createElement('div') + toast.style.cssText = toastStyle + toast.innerText = message + container.appendChild(toast) + + toast.addEventListener('transitionend', (e) => { + // Only on out + if ( + e.target === toast && + e.propertyName === 'height' && + e.elapsedTime > transition_time / 1000 - Number.EPSILON + ) { + log('Transition out') + const totalHeight = Array.from(container.children).reduce( + (acc, child) => acc + child.offsetHeight + 10, // Add spacing of 10px between toasts + 0 + ) + container.style.height = `${totalHeight}px` + + // If there are no toasts left, set the container's height to 0 + if (container.children.length === 0) { + container.style.height = '0' + } + + setTimeout(() => { + container.removeChild(toast) + log('Removed toast from DOM') + }, transition_time) + } else { + log('Transition') + } + }) + + // Fading in the toast + toast.style.opacity = '1' + + // Update container's height to fit new toast + const totalHeight = Array.from(container.children).reduce( + (acc, child) => acc + child.offsetHeight + 10, // Add spacing of 10px between toasts + 0 + ) + container.style.height = `${totalHeight}px` + + // remove the toast after the specified timeout + setTimeout(() => { + // trigger the transitions + toast.style.opacity = '0' + toast.style.height = '0' + toast.style.paddingTop = '0' + toast.style.paddingBottom = '0' + }, timeout - transition_time) +} + +app.registerExtension({ + name: 'mtb.Notify', + setup() { + if (!window.MTB) { + window.MTB = {} + } + + const container = document.createElement('div') + container.id = 'mtb-notify-container' + container.style.cssText = containerStyle + + document.body.appendChild(container) + window.MTB.notify = notify + // window.MTB.notify('Hello world!') + }, +}) From a2ecc11ebde79c2403959bf09c258f3a2465894a Mon Sep 17 00:00:00 2001 From: melMass Date: Mon, 24 Jul 2023 21:42:37 +0200 Subject: [PATCH 61/70] =?UTF-8?q?feat:=20=E2=9A=A1=EF=B8=8F=20use=20notify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit and push wip examples --- .../03-animation_builder-condition-lerp.json | 1230 +++++++++++++++++ examples/04-animation_builder-deforum.json | 1190 ++++++++++++++++ web/mtb_widgets.js | 20 +- 3 files changed, 2439 insertions(+), 1 deletion(-) create mode 100644 examples/03-animation_builder-condition-lerp.json create mode 100644 examples/04-animation_builder-deforum.json diff --git a/examples/03-animation_builder-condition-lerp.json b/examples/03-animation_builder-condition-lerp.json new file mode 100644 index 0000000..bd6e44a --- /dev/null +++ b/examples/03-animation_builder-condition-lerp.json @@ -0,0 +1,1230 @@ +{ + "last_node_id": 79, + "last_link_id": 162, + "nodes": [ + { + "id": 59, + "type": "Reroute", + "pos": [ + -150.35178124999982, + 644.4360633544919 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 124 + } + ], + "outputs": [ + { + "name": "", + "type": "VAE", + "links": [ + 139 + ], + "slot_index": 0 + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 56, + "type": "Reroute", + "pos": [ + -1580.8297949218763, + 644.7740239257807 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 117 + } + ], + "outputs": [ + { + "name": "", + "type": "VAE", + "links": [ + 124 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 57, + "type": "Reroute", + "pos": [ + -673.8297949218747, + -185.22597607421872 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 135 + } + ], + "outputs": [ + { + "name": "", + "type": "MODEL", + "links": [ + 120 + ], + "slot_index": 0 + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 65, + "type": "Reroute", + "pos": [ + -1512.8297949218763, + -181.22597607421872 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 134 + } + ], + "outputs": [ + { + "name": "", + "type": "MODEL", + "links": [ + 135 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 16, + "type": "CheckpointLoaderSimple", + "pos": [ + -2000.8297949218756, + 192.77402392578128 + ], + "size": [ + 315, + 98 + ], + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 134 + ], + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 116 + ], + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 117 + ], + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "revAnimated_v122.safetensors" + ] + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + -482.82979492187513, + -21.22597607421874 + ], + "size": [ + 315, + 474 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 120 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 153 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 6 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 2 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 138 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 5428233522, + "fixed", + 45, + 8, + "euler_ancestral", + "simple", + 1 + ] + }, + { + "id": 19, + "type": "CLIPSetLastLayer", + "pos": [ + -1597, + 209 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 116 + } + ], + "outputs": [ + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 28, + 29, + 149 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPSetLastLayer" + }, + "widgets_values": [ + -2 + ] + }, + { + "id": 20, + "type": "Styles Loader (mtb)", + "pos": [ + -1615, + 404 + ], + "size": [ + 315, + 78 + ], + "flags": { + "collapsed": false + }, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "positive", + "type": "STRING", + "links": [], + "shape": 3 + }, + { + "name": "negative", + "type": "STRING", + "links": [ + 87 + ], + "shape": 3, + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "Styles Loader (mtb)" + }, + "widgets_values": [ + "❌Mel Negatives (general)" + ] + }, + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + -839, + 335 + ], + "size": [ + 210, + 54 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 29 + }, + { + "name": "text", + "type": "STRING", + "link": 87, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 6 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "worst quality, hands, embedding:EasyNegative," + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + -1263, + -42 + ], + "size": [ + 210, + 54 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 28 + }, + { + "name": "text", + "type": "STRING", + "link": 145, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 151 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "A majestic Lion, fur in the wind, (smirk smile)" + ] + }, + { + "id": 72, + "type": "ConditioningAverage ", + "pos": [ + -988, + -13 + ], + "size": [ + 380.4000244140625, + 78 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "name": "conditioning_to", + "type": "CONDITIONING", + "link": 151 + }, + { + "name": "conditioning_from", + "type": "CONDITIONING", + "link": 152, + "slot_index": 1 + }, + { + "name": "conditioning_to_strength", + "type": "FLOAT", + "link": 154, + "widget": { + "name": "conditioning_to_strength", + "config": [ + "FLOAT", + { + "default": 1, + "min": 0, + "max": 1, + "step": 0.01 + } + ] + }, + "slot_index": 2 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 153 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ConditioningAverage " + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 66, + "type": "VAEDecodeTiled", + "pos": [ + 197, + -20 + ], + "size": [ + 210, + 46 + ], + "flags": { + "collapsed": false + }, + "order": 20, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 138 + }, + { + "name": "vae", + "type": "VAE", + "link": 139, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 161 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecodeTiled" + } + }, + { + "id": 78, + "type": "SaveImage", + "pos": [ + 566.1508954260959, + 5.399725291354571 + ], + "size": [ + 315, + 270 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 161 + } + ], + "properties": {}, + "widgets_values": [ + "mtb_demo-conditional_blend" + ] + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": [ + -969, + 484 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 2 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 768, + 512, + 1 + ] + }, + { + "id": 75, + "type": "PrimitiveNode", + "pos": [ + -1626, + 1228 + ], + "size": [ + 210, + 82 + ], + "flags": {}, + "order": 3, + "mode": 0, + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 156, + 159 + ], + "slot_index": 0, + "widget": { + "name": "total_frames", + "config": [ + "INT", + { + "default": 100, + "min": 0 + } + ] + } + } + ], + "properties": {}, + "widgets_values": [ + 24, + "fixed" + ] + }, + { + "id": 74, + "type": "Get Batch From History (mtb)", + "pos": [ + -594, + 914 + ], + "size": [ + 315, + 102 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "enable", + "type": "BOOL", + "link": 155 + }, + { + "name": "count", + "type": "INT", + "link": 159, + "widget": { + "name": "count", + "config": [ + "INT", + { + "default": 1, + "min": 0 + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "i", + "type": "IMAGE", + "links": [ + 160, + 162 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Get Batch From History (mtb)" + }, + "widgets_values": [ + 24, + 0, + false + ] + }, + { + "id": 77, + "type": "Export To Prores (mtb)", + "pos": [ + -57, + 911 + ], + "size": [ + 315, + 82 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 160, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "VIDEO", + "type": "VIDEO", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "Export To Prores (mtb)" + }, + "widgets_values": [ + 12, + "export" + ] + }, + { + "id": 70, + "type": "CLIPTextEncode", + "pos": [ + -1246, + 72 + ], + "size": [ + 210, + 54 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 149, + "slot_index": 0 + }, + { + "name": "text", + "type": "STRING", + "link": 150, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 152 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "A majestic Lion, fur in the wind, (smirk smile)" + ] + }, + { + "id": 68, + "type": "Text box", + "pos": [ + -1871, + 9 + ], + "size": [ + 217.2119140625, + 122.18632507324219 + ], + "flags": {}, + "order": 4, + "mode": 0, + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 141, + 150 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Text box" + }, + "widgets_values": [ + "A cinematic shot of a blue car, high speed, blood trail" + ] + }, + { + "id": 69, + "type": "String Replace (mtb)", + "pos": [ + -1566, + -58 + ], + "size": [ + 210, + 82 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "string", + "type": "STRING", + "link": 141 + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 145 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String Replace (mtb)" + }, + "widgets_values": [ + "blue", + "yellow" + ] + }, + { + "id": 73, + "type": "Animation Builder (mtb)", + "pos": [ + -1260, + 850 + ], + "size": [ + 211.60000610351562, + 306 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "total_frames", + "type": "INT", + "link": 156, + "widget": { + "name": "total_frames", + "config": [ + "INT", + { + "default": 100, + "min": 0 + } + ] + } + } + ], + "outputs": [ + { + "name": "frame", + "type": "INT", + "links": null, + "shape": 3, + "slot_index": 0 + }, + { + "name": "0-1 (scaled)", + "type": "FLOAT", + "links": [ + 154 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "count", + "type": "INT", + "links": null, + "shape": 3 + }, + { + "name": "loop_ended", + "type": "BOOL", + "links": [ + 155 + ], + "shape": 3, + "slot_index": 3 + } + ], + "properties": { + "Node name for S&R": "Animation Builder (mtb)" + }, + "widgets_values": [ + 24, + 1, + 1, + 24, + 1, + "raw: 24\nframe: 0", + "Done 😎!", + "reset", + "queue" + ] + }, + { + "id": 79, + "type": "Save Gif (mtb)", + "pos": [ + -33, + 1041 + ], + "size": [ + 210, + 338.00006103515625 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 162 + }, + { + "name": "pingpong", + "type": "BOOL", + "link": null + } + ], + "properties": { + "Node name for S&R": "Save Gif (mtb)" + }, + "widgets_values": [ + 12, + 1, + true + ] + } + ], + "links": [ + [ + 2, + 5, + 0, + 3, + 3, + "LATENT" + ], + [ + 6, + 7, + 0, + 3, + 2, + "CONDITIONING" + ], + [ + 28, + 19, + 0, + 6, + 0, + "CLIP" + ], + [ + 29, + 19, + 0, + 7, + 0, + "CLIP" + ], + [ + 87, + 20, + 1, + 7, + 1, + "STRING" + ], + [ + 116, + 16, + 1, + 19, + 0, + "CLIP" + ], + [ + 117, + 16, + 2, + 56, + 0, + "*" + ], + [ + 120, + 57, + 0, + 3, + 0, + "MODEL" + ], + [ + 124, + 56, + 0, + 59, + 0, + "*" + ], + [ + 134, + 16, + 0, + 65, + 0, + "*" + ], + [ + 135, + 65, + 0, + 57, + 0, + "*" + ], + [ + 138, + 3, + 0, + 66, + 0, + "LATENT" + ], + [ + 139, + 59, + 0, + 66, + 1, + "VAE" + ], + [ + 141, + 68, + 0, + 69, + 0, + "STRING" + ], + [ + 145, + 69, + 0, + 6, + 1, + "STRING" + ], + [ + 149, + 19, + 0, + 70, + 0, + "CLIP" + ], + [ + 150, + 68, + 0, + 70, + 1, + "STRING" + ], + [ + 151, + 6, + 0, + 72, + 0, + "CONDITIONING" + ], + [ + 152, + 70, + 0, + 72, + 1, + "CONDITIONING" + ], + [ + 153, + 72, + 0, + 3, + 1, + "CONDITIONING" + ], + [ + 154, + 73, + 1, + 72, + 2, + "FLOAT" + ], + [ + 155, + 73, + 3, + 74, + 0, + "BOOL" + ], + [ + 156, + 75, + 0, + 73, + 0, + "INT" + ], + [ + 159, + 75, + 0, + 74, + 1, + "INT" + ], + [ + 160, + 74, + 0, + 77, + 0, + "IMAGE" + ], + [ + 161, + 66, + 0, + 78, + 0, + "IMAGE" + ], + [ + 162, + 74, + 0, + 79, + 0, + "IMAGE" + ] + ], + "groups": [ + { + "title": "Txt2Img", + "bounding": [ + -2061, + -234, + 1932, + 973 + ], + "color": "#a1309b", + "locked": false + }, + { + "title": "Save Intermediate Image", + "bounding": [ + 150, + -116, + 303, + 213 + ], + "color": "#3f789e", + "locked": false + } + ], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file diff --git a/examples/04-animation_builder-deforum.json b/examples/04-animation_builder-deforum.json new file mode 100644 index 0000000..0b66feb --- /dev/null +++ b/examples/04-animation_builder-deforum.json @@ -0,0 +1,1190 @@ +{ + "last_node_id": 39, + "last_link_id": 69, + "nodes": [ + { + "id": 11, + "type": "Get Batch From History (mtb)", + "pos": [ + -828, + 522 + ], + "size": [ + 235.1999969482422, + 118 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "passthrough_image", + "type": "IMAGE", + "link": 10 + }, + { + "name": "enable", + "type": "BOOL", + "link": 9, + "widget": { + "name": "enable", + "config": [ + "BOOL", + { + "default": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "i", + "type": "IMAGE", + "links": [ + 26 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Get Batch From History (mtb)" + }, + "widgets_values": [ + false, + 1, + 0 + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 24, + "type": "Note", + "pos": [ + -827, + 406 + ], + "size": [ + 233.25148010253906, + 82.53218841552734 + ], + "flags": {}, + "order": 0, + "mode": 0, + "properties": { + "text": "" + }, + "widgets_values": [ + "On first frame we get the init image, on all subsequent ones the feedback from the previous queue item" + ], + "color": "#223", + "bgcolor": "#335", + "shape": 1 + }, + { + "id": 12, + "type": "Int To Bool (mtb)", + "pos": [ + -1057, + 583 + ], + "size": [ + 210, + 36.36605696244669 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "int", + "type": "INT", + "link": 34, + "widget": { + "name": "int", + "config": [ + "INT", + { + "default": 0 + } + ] + }, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "BOOL", + "type": "BOOL", + "links": [ + 9 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Int To Bool (mtb)" + }, + "widgets_values": [ + 29 + ], + "color": "#222", + "bgcolor": "#000" + }, + { + "id": 10, + "type": "LoadImage", + "pos": [ + -1409, + 524 + ], + "size": [ + 315, + 314 + ], + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 10 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "example.png", + "image" + ], + "color": "#432", + "bgcolor": "#653", + "shape": 1 + }, + { + "id": 18, + "type": "Get Batch From History (mtb)", + "pos": [ + -960, + 1257 + ], + "size": [ + 235.1999969482422, + 118 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "passthrough_image", + "type": "IMAGE", + "link": null + }, + { + "name": "enable", + "type": "BOOL", + "link": 31, + "widget": { + "name": "enable", + "config": [ + "BOOL", + { + "default": true + } + ] + }, + "slot_index": 1 + }, + { + "name": "count", + "type": "INT", + "link": 44, + "widget": { + "name": "count", + "config": [ + "INT", + { + "default": 1, + "min": 0 + } + ] + } + } + ], + "outputs": [ + { + "name": "i", + "type": "IMAGE", + "links": [ + 17, + 18 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Get Batch From History (mtb)" + }, + "widgets_values": [ + true, + 30, + 0 + ] + }, + { + "id": 20, + "type": "Export To Prores (mtb)", + "pos": [ + -612, + 1818 + ], + "size": [ + 292.4239807128906, + 93.884033203125 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 17 + } + ], + "outputs": [ + { + "name": "VIDEO", + "type": "VIDEO", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "Export To Prores (mtb)" + }, + "widgets_values": [ + 12, + "export" + ] + }, + { + "id": 35, + "type": "CLIPTextEncode", + "pos": [ + -118, + 331 + ], + "size": [ + 210, + 54 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 60 + }, + { + "name": "text", + "type": "STRING", + "link": 66, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + } + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 54 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "" + ] + }, + { + "id": 9, + "type": "CheckpointLoaderSimple", + "pos": [ + -558, + 114 + ], + "size": [ + 301.23302978515596, + 98 + ], + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 62 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 59, + 60 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 58, + 69 + ], + "shape": 3, + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "revAnimated_v122.safetensors" + ] + }, + { + "id": 37, + "type": "VAEEncode", + "pos": [ + -125, + 236 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "pixels", + "type": "IMAGE", + "link": 57 + }, + { + "name": "vae", + "type": "VAE", + "link": 58, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 55 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEEncode" + } + }, + { + "id": 34, + "type": "CLIPTextEncode", + "pos": [ + -124, + 84 + ], + "size": [ + 210, + 96 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 59 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 53 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "A blonde doll with a pink dress sleeping, draping, wrinkles" + ] + }, + { + "id": 33, + "type": "Text box", + "pos": [ + -497, + 349 + ], + "size": [ + 294, + 95.12843764123829 + ], + "flags": {}, + "order": 3, + "mode": 0, + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 66 + ], + "shape": 3, + "slot_index": 0 + } + ], + "title": "❌Mel Negatives (general) (Negative)", + "properties": { + "Node name for S&R": "Text box" + }, + "widgets_values": [ + "embedding:EasyNegative, embedding:EasyNegativeV2, watermark, text, deformed" + ] + }, + { + "id": 39, + "type": "Reroute", + "pos": [ + -156, + 908 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 67 + } + ], + "outputs": [ + { + "name": "", + "type": "INT", + "links": [ + 68 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 14, + "type": "Transform Image (mtb)", + "pos": [ + -527, + 520 + ], + "size": [ + 315, + 178 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 26 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 57 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Transform Image (mtb)" + }, + "widgets_values": [ + 0, + 5, + 1.03, + 1, + 0, + "reflect" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 36, + "type": "KSampler", + "pos": [ + 223, + 262 + ], + "size": [ + 315, + 442 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 62, + "slot_index": 0 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 53 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 54 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 55 + }, + { + "name": "denoise", + "type": "FLOAT", + "link": 63, + "widget": { + "name": "denoise", + "config": [ + "FLOAT", + { + "default": 1, + "min": 0, + "max": 1, + "step": 0.01 + } + ] + }, + "slot_index": 4 + }, + { + "name": "seed", + "type": "INT", + "link": 68, + "widget": { + "name": "seed", + "config": [ + "INT", + { + "default": 0, + "min": 0, + "max": 18446744073709552000 + } + ] + } + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 56 + ], + "shape": 3, + "slot_index": 0, + "color": "#FF9CF9" + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 61876307854624, + "randomize", + 15, + 8, + "euler_ancestral", + "normal", + 0.6 + ] + }, + { + "id": 15, + "type": "SaveImage", + "pos": [ + 782, + 259 + ], + "size": [ + 330.11123461913985, + 378.1239961059566 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 65 + } + ], + "properties": {}, + "widgets_values": [ + "ComfyUI" + ] + }, + { + "id": 19, + "type": "Save Gif (mtb)", + "pos": [ + -613, + 1256 + ], + "size": [ + 356.17758847656205, + 471.87858076171824 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 18 + } + ], + "properties": { + "Node name for S&R": "Save Gif (mtb)" + }, + "widgets_values": [ + 12, + 1, + true, + "/view?filename=7964e7cc8b.gif&subfolder=&type=output" + ] + }, + { + "id": 31, + "type": "PrimitiveNode", + "pos": [ + -2060, + 1291 + ], + "size": [ + 210, + 82 + ], + "flags": {}, + "order": 4, + "mode": 0, + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 43, + 44 + ], + "widget": { + "name": "total_frames", + "config": [ + "INT", + { + "default": 100, + "min": 0 + } + ] + }, + "slot_index": 0 + } + ], + "title": "total_frames", + "properties": {}, + "widgets_values": [ + 30, + "fixed" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 38, + "type": "VAEDecode", + "pos": [ + 556, + 260 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 56 + }, + { + "name": "vae", + "type": "VAE", + "link": 69, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 65 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + } + }, + { + "id": 22, + "type": "Fit Number (mtb)", + "pos": [ + -647, + 882 + ], + "size": [ + 232.28509687500036, + 166 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "value", + "type": "FLOAT", + "link": 27 + } + ], + "outputs": [ + { + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 63 + ], + "shape": 3, + "slot_index": 0 + } + ], + "title": "Fit Number (mtb) - Denoise", + "properties": { + "Node name for S&R": "Fit Number (mtb)" + }, + "widgets_values": [ + true, + 0, + 1, + 0.3, + 0.6 + ] + }, + { + "id": 17, + "type": "Animation Builder (mtb)", + "pos": [ + -1309, + 883 + ], + "size": [ + 211.60000610351562, + 294 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "total_frames", + "type": "INT", + "link": 43, + "widget": { + "name": "total_frames", + "config": [ + "INT", + { + "default": 100, + "min": 0 + } + ] + }, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "frame", + "type": "INT", + "links": [ + 34 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "0-1 (scaled)", + "type": "FLOAT", + "links": [ + 27 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "count", + "type": "INT", + "links": [ + 67 + ], + "shape": 3, + "slot_index": 2 + }, + { + "name": "loop_ended", + "type": "BOOL", + "links": [ + 31 + ], + "shape": 3, + "slot_index": 3 + } + ], + "properties": { + "Node name for S&R": "Animation Builder (mtb)" + }, + "widgets_values": [ + 30, + 1, + 1, + 0, + 0, + "Idle", + "Iteration: Idle", + "reset", + "queue" + ], + "color": "#232", + "bgcolor": "#353", + "shape": 1 + } + ], + "links": [ + [ + 9, + 12, + 0, + 11, + 1, + "BOOL" + ], + [ + 10, + 10, + 0, + 11, + 0, + "IMAGE" + ], + [ + 17, + 18, + 0, + 20, + 0, + "IMAGE" + ], + [ + 18, + 18, + 0, + 19, + 0, + "IMAGE" + ], + [ + 26, + 11, + 0, + 14, + 0, + "IMAGE" + ], + [ + 27, + 17, + 1, + 22, + 0, + "FLOAT" + ], + [ + 31, + 17, + 3, + 18, + 1, + "BOOL" + ], + [ + 34, + 17, + 0, + 12, + 0, + "INT" + ], + [ + 43, + 31, + 0, + 17, + 0, + "INT" + ], + [ + 44, + 31, + 0, + 18, + 2, + "INT" + ], + [ + 53, + 34, + 0, + 36, + 1, + "CONDITIONING" + ], + [ + 54, + 35, + 0, + 36, + 2, + "CONDITIONING" + ], + [ + 55, + 37, + 0, + 36, + 3, + "LATENT" + ], + [ + 56, + 36, + 0, + 38, + 0, + "LATENT" + ], + [ + 57, + 14, + 0, + 37, + 0, + "IMAGE" + ], + [ + 58, + 9, + 2, + 37, + 1, + "VAE" + ], + [ + 59, + 9, + 1, + 34, + 0, + "CLIP" + ], + [ + 60, + 9, + 1, + 35, + 0, + "CLIP" + ], + [ + 62, + 9, + 0, + 36, + 0, + "MODEL" + ], + [ + 63, + 22, + 0, + 36, + 4, + "FLOAT" + ], + [ + 65, + 38, + 0, + 15, + 0, + "IMAGE" + ], + [ + 66, + 33, + 0, + 35, + 1, + "STRING" + ], + [ + 67, + 17, + 2, + 39, + 0, + "*" + ], + [ + 68, + 39, + 0, + 36, + 5, + "INT" + ], + [ + 69, + 9, + 2, + 38, + 1, + "VAE" + ] + ], + "groups": [ + { + "title": "Video Output", + "bounding": [ + -702, + 1161, + 516, + 773 + ], + "color": "#3f789e", + "locked": false + }, + { + "title": "START THE QUEUE BY CLICKLING HERE 👆", + "bounding": [ + -1611, + 1196, + 521, + 80 + ], + "color": "#8A8", + "locked": false + } + ], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file diff --git a/web/mtb_widgets.js b/web/mtb_widgets.js index 11dfdb4..4ea2136 100644 --- a/web/mtb_widgets.js +++ b/web/mtb_widgets.js @@ -796,8 +796,14 @@ const mtb_widgets = { const run_button = this.addWidget('button', `Queue`, 'queue', () => { onReset() // this could maybe be a setting or checkbox app.queuePrompt(0, total_frames.value * loop_count.value) + window.MTB?.notify?.( + `Started a queue of ${total_frames.value} frames (for ${ + loop_count.value + } loop, so ${total_frames.value * loop_count.value})`, + 5000 + ) }) - + this.onRemoved = () => { for (const w of this.widgets) { if (w.canvas) { @@ -880,11 +886,17 @@ const mtb_widgets = { const style = await getStyle(this) if (style && style.length >= 1) { if (style[0]) { + window.MTB?.notify?.( + `Extracted positive from ${this.widgets[0].value}` + ) const tn = LiteGraph.createNode('Text box') app.graph.add(tn) tn.title = `${this.widgets[0].value} (Positive)` tn.widgets[0].value = style[0] } else { + window.MTB?.notify?.( + `No positive to extract for ${this.widgets[0].value}` + ) } } }, @@ -895,11 +907,17 @@ const mtb_widgets = { const style = await getStyle(this) if (style && style.length >= 2) { if (style[1]) { + window.MTB?.notify?.( + `Extracted negative from ${this.widgets[0].value}` + ) const tn = LiteGraph.createNode('Text box') app.graph.add(tn) tn.title = `${this.widgets[0].value} (Negative)` tn.widgets[0].value = style[1] } else { + window.MTB.notify( + `No negative to extract for ${this.widgets[0].value}` + ) } } }, From 9a4eda3ef573bf382c13515f67ae8a415bf61abd Mon Sep 17 00:00:00 2001 From: melMass Date: Mon, 24 Jul 2023 21:43:55 +0200 Subject: [PATCH 62/70] =?UTF-8?q?feat:=20=F0=9F=9A=A7=20jupyter=20seems=20?= =?UTF-8?q?to=20require=20an=20=5F=5Finit=5F=5F=20there?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nodes/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 nodes/__init__.py diff --git a/nodes/__init__.py b/nodes/__init__.py new file mode 100644 index 0000000..e69de29 From 3b9190a69b002b8933c097fd6655bb4fe07264d2 Mon Sep 17 00:00:00 2001 From: melMass Date: Mon, 24 Jul 2023 21:46:44 +0200 Subject: [PATCH 63/70] =?UTF-8?q?ci:=20=F0=9F=9A=80=20Remove=20large=20fil?= =?UTF-8?q?es=20from=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit following @WASasquatch advice --- .github/workflows/release.yml | 22 ++++++++++++++++++++++ .release_ignore | 3 +++ 2 files changed, 25 insertions(+) create mode 100644 .release_ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6763945..48e7289 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,6 +29,28 @@ jobs: with: submodules: "recursive" path: ${{ env.repo_name }} + + # - name: 📝 Prepare file with paths to remove + # run: | + # find ${{ env.repo_name }} -type f -size +10M > .release_ignore + # find ${{ env.repo_name }} -type d -empty >> .release_ignore + # shell: bash + + - name: 🗑️ Remove files and directories listed in .release_ignore + shell: bash + run: | + if [ -f "${{ env.repo_name }}/.release_ignore" ]; then + while IFS= read -r entry; do + if [ -f "${{ env.repo_name }}/$entry" ]; then + rm "${{ env.repo_name }}/$entry" + elif [ -d "${{ env.repo_name }}/$entry" ]; then + rm -r "${{ env.repo_name }}/$entry" + fi + done < "${{ env.repo_name }}/.release_ignore" + else + echo "No .release_ignore file found. Skipping removal of files and directories." + fi + - name: 📦 Building custom comfy nodes shell: bash run: | diff --git a/.release_ignore b/.release_ignore new file mode 100644 index 0000000..31f6c67 --- /dev/null +++ b/.release_ignore @@ -0,0 +1,3 @@ +extern/frame_interpolation/moment.gif +extern/frame_interpolation/photos +extern/GFPGAN/inputs \ No newline at end of file From cf865529ab64b350cd7af964b41160e7d130d12d Mon Sep 17 00:00:00 2001 From: melMass Date: Mon, 24 Jul 2023 21:53:36 +0200 Subject: [PATCH 64/70] =?UTF-8?q?chore:=20=F0=9F=9A=80=20bump=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 5cf2c58..11a713d 100644 --- a/__init__.py +++ b/__init__.py @@ -24,7 +24,7 @@ NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} NODE_CLASS_MAPPINGS_DEBUG = {} -__version__ = "0.1.0" +__version__ = "0.1.1" def extract_nodes_from_source(filename): From c0cc5572d8c727568eca8a3d0f116a1f540c31ff Mon Sep 17 00:00:00 2001 From: melMass Date: Mon, 24 Jul 2023 22:24:38 +0200 Subject: [PATCH 65/70] =?UTF-8?q?ci:=20=F0=9F=90=9B=20fix=20size?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit it was ignoring the last line, I also ignore the git folder itself --- .github/workflows/release.yml | 20 +++++++++++++------- .release_ignore | 3 ++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48e7289..36bdfb0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,14 +39,20 @@ jobs: - name: 🗑️ Remove files and directories listed in .release_ignore shell: bash run: | - if [ -f "${{ env.repo_name }}/.release_ignore" ]; then - while IFS= read -r entry; do - if [ -f "${{ env.repo_name }}/$entry" ]; then - rm "${{ env.repo_name }}/$entry" - elif [ -d "${{ env.repo_name }}/$entry" ]; then - rm -r "${{ env.repo_name }}/$entry" + release_ignore="${{ env.repo_name }}/.release_ignore" + if [ -f "$release_ignore" ]; then + while IFS= read -r entry || [ -n "$entry" ]; do + target="${{ env.repo_name }}/$entry" + if [ -e "$target" ]; then + if [ -f "$target" ]; then + rm "$target" + elif [ -d "$target" ]; then + rm -r "$target" + fi + else + echo "Warning: $entry does not exist in the repository. Skipping removal." fi - done < "${{ env.repo_name }}/.release_ignore" + done < "$release_ignore" else echo "No .release_ignore file found. Skipping removal of files and directories." fi diff --git a/.release_ignore b/.release_ignore index 31f6c67..a1ad29c 100644 --- a/.release_ignore +++ b/.release_ignore @@ -1,3 +1,4 @@ extern/frame_interpolation/moment.gif extern/frame_interpolation/photos -extern/GFPGAN/inputs \ No newline at end of file +extern/GFPGAN/inputs +.git \ No newline at end of file From 3dfe98c7957df48723380de85e1242a424ec23de Mon Sep 17 00:00:00 2001 From: melMass Date: Mon, 24 Jul 2023 23:43:02 +0200 Subject: [PATCH 66/70] =?UTF-8?q?fix:=20=E2=9A=A1=EF=B8=8F=20update=20exam?= =?UTF-8?q?ples=20to=20match=20wiki?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/02-film_interpolation.json | 848 ++++++------- .../03-animation_builder-condition-lerp.json | 1082 +++++++++-------- examples/04-animation_builder-deforum.json | 33 +- 3 files changed, 1002 insertions(+), 961 deletions(-) diff --git a/examples/02-film_interpolation.json b/examples/02-film_interpolation.json index 218de4f..eb8ef7e 100644 --- a/examples/02-film_interpolation.json +++ b/examples/02-film_interpolation.json @@ -1,6 +1,6 @@ { - "last_node_id": 82, - "last_link_id": 165, + "last_node_id": 86, + "last_link_id": 171, "nodes": [ { "id": 59, @@ -14,7 +14,7 @@ 26 ], "flags": {}, - "order": 15, + "order": 13, "mode": 0, "inputs": [ { @@ -51,7 +51,7 @@ 26 ], "flags": {}, - "order": 10, + "order": 8, "mode": 0, "inputs": [ { @@ -74,41 +74,6 @@ "horizontal": false } }, - { - "id": 65, - "type": "Reroute", - "pos": [ - -1990, - 160 - ], - "size": [ - 75, - 26 - ], - "flags": {}, - "order": 8, - "mode": 0, - "inputs": [ - { - "name": "", - "type": "*", - "link": 134 - } - ], - "outputs": [ - { - "name": "", - "type": "MODEL", - "links": [ - 135 - ] - } - ], - "properties": { - "showOutputText": false, - "horizontal": false - } - }, { "id": 57, "type": "Reroute", @@ -121,7 +86,7 @@ 26 ], "flags": {}, - "order": 12, + "order": 10, "mode": 0, "inputs": [ { @@ -180,39 +145,6 @@ 1 ] }, - { - "id": 67, - "type": "Text", - "pos": [ - -2280, - 250 - ], - "size": [ - 315, - 58 - ], - "flags": {}, - "order": 1, - "mode": 0, - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 142, - 164 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "Text" - }, - "widgets_values": [ - "Photo of a Face of a man (looking down), rim lighting, tokyo 1987" - ] - }, { "id": 3, "type": "KSampler", @@ -225,7 +157,7 @@ 474 ], "flags": {}, - "order": 17, + "order": 15, "mode": 0, "inputs": [ { @@ -279,7 +211,7 @@ "Node name for S&R": "KSampler" }, "widgets_values": [ - 542821171532978, + 45, "fixed", 45, 8, @@ -288,49 +220,6 @@ 1 ] }, - { - "id": 72, - "type": "PrimitiveNode", - "pos": [ - -1290, - 880 - ], - "size": [ - 210, - 82 - ], - "flags": {}, - "order": 2, - "mode": 0, - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 148, - 150 - ], - "widget": { - "name": "seed", - "config": [ - "INT", - { - "default": 0, - "min": 0, - "max": 18446744073709552000 - } - ] - }, - "slot_index": 0 - } - ], - "title": "seed", - "properties": {}, - "widgets_values": [ - 542821171532978, - "fixed" - ] - }, { "id": 79, "type": "Load Film Model (mtb)", @@ -343,7 +232,7 @@ 58 ], "flags": {}, - "order": 3, + "order": 1, "mode": 0, "outputs": [ { @@ -400,7 +289,7 @@ 75.28300476074219 ], "flags": {}, - "order": 16, + "order": 18, "mode": 0, "inputs": [ { @@ -412,7 +301,7 @@ { "name": "text", "type": "STRING", - "link": 151, + "link": 171, "widget": { "name": "text", "config": [ @@ -453,7 +342,7 @@ 58 ], "flags": {}, - "order": 9, + "order": 7, "mode": 0, "inputs": [ { @@ -494,7 +383,7 @@ 54 ], "flags": {}, - "order": 13, + "order": 11, "mode": 0, "inputs": [ { @@ -505,7 +394,7 @@ { "name": "text", "type": "STRING", - "link": 142, + "link": 167, "widget": { "name": "text", "config": [ @@ -534,175 +423,6 @@ "Face of a man (looking down), rim lighting, tokyo 1987" ] }, - { - "id": 7, - "type": "CLIPTextEncode", - "pos": [ - -1320, - 430 - ], - "size": [ - 210, - 54 - ], - "flags": {}, - "order": 14, - "mode": 0, - "inputs": [ - { - "name": "clip", - "type": "CLIP", - "link": 29 - }, - { - "name": "text", - "type": "STRING", - "link": 87, - "widget": { - "name": "text", - "config": [ - "STRING", - { - "multiline": true - } - ] - }, - "slot_index": 1 - } - ], - "outputs": [ - { - "name": "CONDITIONING", - "type": "CONDITIONING", - "links": [ - 6, - 145 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "CLIPTextEncode" - }, - "widgets_values": [ - "worst quality, hands, embedding:EasyNegative," - ] - }, - { - "id": 20, - "type": "Styles Loader (mtb)", - "pos": [ - -1650, - 500 - ], - "size": [ - 285.32098388671875, - 78 - ], - "flags": { - "collapsed": false - }, - "order": 4, - "mode": 0, - "outputs": [ - { - "name": "positive", - "type": "STRING", - "links": [], - "shape": 3 - }, - { - "name": "negative", - "type": "STRING", - "links": [ - 87 - ], - "shape": 3, - "slot_index": 1 - } - ], - "properties": { - "Node name for S&R": "Styles Loader (mtb)" - }, - "widgets_values": [ - "❌Low Token" - ] - }, - { - "id": 81, - "type": "Reroute", - "pos": [ - -1740, - -10 - ], - "size": [ - 75, - 26 - ], - "flags": {}, - "order": 7, - "mode": 0, - "inputs": [ - { - "name": "", - "type": "*", - "link": 164 - } - ], - "outputs": [ - { - "name": "", - "type": "STRING", - "links": [ - 165 - ] - } - ], - "properties": { - "showOutputText": false, - "horizontal": false - } - }, - { - "id": 69, - "type": "String Replace (mtb)", - "pos": [ - -1000, - 0 - ], - "size": [ - 315, - 82 - ], - "flags": {}, - "order": 11, - "mode": 0, - "inputs": [ - { - "name": "string", - "type": "STRING", - "link": 165 - } - ], - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 151 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "String Replace (mtb)" - }, - "widgets_values": [ - "(looking down)", - "(looking up)" - ] - }, { "id": 75, "type": "VAEDecodeTiled", @@ -748,52 +468,6 @@ "Node name for S&R": "VAEDecodeTiled" } }, - { - "id": 78, - "type": "Film Interpolation (mtb)", - "pos": [ - 1140, - 520 - ], - "size": [ - 315, - 78 - ], - "flags": {}, - "order": 22, - "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 160, - "slot_index": 0 - }, - { - "name": "film_model", - "type": "FILM_MODEL", - "link": 161, - "slot_index": 1 - } - ], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 162 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "Film Interpolation (mtb)" - }, - "widgets_values": [ - 4 - ] - }, { "id": 70, "type": "KSampler", @@ -806,7 +480,7 @@ 474 ], "flags": {}, - "order": 18, + "order": 19, "mode": 0, "inputs": [ { @@ -862,7 +536,7 @@ "Node name for S&R": "KSampler" }, "widgets_values": [ - 542821171532978, + 45, "fixed", 45, 8, @@ -883,7 +557,7 @@ 98 ], "flags": {}, - "order": 5, + "order": 2, "mode": 0, "outputs": [ { @@ -918,29 +592,6 @@ "revAnimated_v122.safetensors" ] }, - { - "id": 82, - "type": "Note", - "pos": [ - -398, - 25 - ], - "size": [ - 500.61550625887776, - 270.54209761186075 - ], - "flags": {}, - "order": 6, - "mode": 0, - "properties": { - "text": "THIS LOOKS A BIT MESSY BUT WE BASICALLY USE THE SAME INPUTS (MODEL, SEED etc), ONLY STRING REPLACING THE PROMPT, LOOKING DOWN -> LOOKING UP" - }, - "widgets_values": [ - "THIS LOOKS A BIT MESSY BUT WE BASICALLY USE THE SAME INPUTS (MODEL, SEED etc), ONLY STRING REPLACING THE PROMPT, LOOKING DOWN -> LOOKING UP\n" - ], - "color": "#222", - "bgcolor": "#000" - }, { "id": 66, "type": "VAEDecodeTiled", @@ -955,7 +606,7 @@ "flags": { "collapsed": false }, - "order": 19, + "order": 17, "mode": 0, "inputs": [ { @@ -1028,29 +679,394 @@ } }, { - "id": 76, - "type": "PreviewImage", + "id": 65, + "type": "Reroute", "pos": [ - 1517, - 207 + -1810, + 148 ], "size": [ - 511.33429483925056, - 675.8856963258129 + 75, + 26 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 134 + } + ], + "outputs": [ + { + "name": "", + "type": "MODEL", + "links": [ + 135 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 81, + "type": "Reroute", + "pos": [ + -1821, + 1 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 166 + } + ], + "outputs": [ + { + "name": "", + "type": "STRING", + "links": [ + 165 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 84, + "type": "Text box", + "pos": [ + -1669, + 498 + ], + "size": [ + 294, + 104.4554216999652 + ], + "flags": {}, + "order": 3, + "mode": 0, + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 168 + ], + "shape": 3, + "slot_index": 0 + } + ], + "title": "❌Mel Negatives (general) (Negative)", + "properties": { + "Node name for S&R": "Text box" + }, + "widgets_values": [ + "embedding:EasyNegative, embedding:EasyNegativeV2, watermark, text, deformed, bad anatomy, disfigured" + ] + }, + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + -1317, + 424 + ], + "size": [ + 210, + 54 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 29 + }, + { + "name": "text", + "type": "STRING", + "link": 168, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 6, + 145 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "worst quality, hands, embedding:EasyNegative," + ] + }, + { + "id": 78, + "type": "Film Interpolation (mtb)", + "pos": [ + 1140, + 520 + ], + "size": [ + 315, + 78 + ], + "flags": {}, + "order": 22, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 160, + "slot_index": 0 + }, + { + "name": "film_model", + "type": "FILM_MODEL", + "link": 161, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 169 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Film Interpolation (mtb)" + }, + "widgets_values": [ + 4 + ] + }, + { + "id": 85, + "type": "Save Gif (mtb)", + "pos": [ + 1546, + 401 + ], + "size": [ + 210, + 336 ], "flags": {}, "order": 24, "mode": 0, "inputs": [ { - "name": "images", + "name": "image", "type": "IMAGE", - "link": 162 + "link": 169 } ], "properties": { - "Node name for S&R": "PreviewImage" - } + "Node name for S&R": "Save Gif (mtb)" + }, + "widgets_values": [ + 12, + 0.7, + true, + "/view?filename=eda71478da.gif&subfolder=&type=output" + ] + }, + { + "id": 72, + "type": "PrimitiveNode", + "pos": [ + -1290, + 880 + ], + "size": [ + 210, + 82 + ], + "flags": {}, + "order": 4, + "mode": 0, + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 148, + 150 + ], + "widget": { + "name": "seed", + "config": [ + "INT", + { + "default": 0, + "min": 0, + "max": 18446744073709552000 + } + ] + }, + "slot_index": 0 + } + ], + "title": "seed", + "properties": {}, + "widgets_values": [ + 45, + "fixed" + ] + }, + { + "id": 83, + "type": "Text box", + "pos": [ + -2456, + 236 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 5, + "mode": 0, + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 166, + 167 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Text box" + }, + "widgets_values": [ + "Close up photo of the face of a man (looking down, and frowning), rim lighting, Tokyo 1987, Bernard, over a blue sky, blue eyes and a long beard" + ] + }, + { + "id": 69, + "type": "String Replace (mtb)", + "pos": [ + -1442, + -1 + ], + "size": [ + 315, + 82 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "string", + "type": "STRING", + "link": 165 + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 170 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String Replace (mtb)" + }, + "widgets_values": [ + "looking down", + "looking up" + ] + }, + { + "id": 86, + "type": "String Replace (mtb)", + "pos": [ + -1082, + 1 + ], + "size": [ + 315, + 82 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "string", + "type": "STRING", + "link": 170 + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 171 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String Replace (mtb)" + }, + "widgets_values": [ + "frowning", + "smiling" + ] } ], "links": [ @@ -1094,14 +1110,6 @@ 0, "CLIP" ], - [ - 87, - 20, - 1, - 7, - 1, - "STRING" - ], [ 116, 16, @@ -1166,14 +1174,6 @@ 1, "VAE" ], - [ - 142, - 67, - 0, - 6, - 1, - "STRING" - ], [ 143, 57, @@ -1214,14 +1214,6 @@ 4, "INT" ], - [ - 151, - 69, - 0, - 71, - 1, - "STRING" - ], [ 152, 19, @@ -1286,14 +1278,6 @@ 1, "FILM_MODEL" ], - [ - 162, - 78, - 0, - 76, - 0, - "IMAGE" - ], [ 163, 77, @@ -1303,19 +1287,59 @@ "IMAGE" ], [ - 164, - 67, + 165, + 81, + 0, + 69, + 0, + "STRING" + ], + [ + 166, + 83, 0, 81, 0, "*" ], [ - 165, - 81, + 167, + 83, 0, + 6, + 1, + "STRING" + ], + [ + 168, + 84, + 0, + 7, + 1, + "STRING" + ], + [ + 169, + 78, + 0, + 85, + 0, + "IMAGE" + ], + [ + 170, 69, 0, + 86, + 0, + "STRING" + ], + [ + 171, + 86, + 0, + 71, + 1, "STRING" ] ], diff --git a/examples/03-animation_builder-condition-lerp.json b/examples/03-animation_builder-condition-lerp.json index bd6e44a..7262e17 100644 --- a/examples/03-animation_builder-condition-lerp.json +++ b/examples/03-animation_builder-condition-lerp.json @@ -1,6 +1,6 @@ { - "last_node_id": 79, - "last_link_id": 162, + "last_node_id": 82, + "last_link_id": 171, "nodes": [ { "id": 59, @@ -191,12 +191,419 @@ "revAnimated_v122.safetensors" ] }, + { + "id": 19, + "type": "CLIPSetLastLayer", + "pos": [ + -1597, + 209 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 116 + } + ], + "outputs": [ + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 28, + 29, + 149 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPSetLastLayer" + }, + "widgets_values": [ + -2 + ] + }, + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + -839, + 335 + ], + "size": [ + 210, + 54 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 29 + }, + { + "name": "text", + "type": "STRING", + "link": 163, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 6 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "worst quality, hands, embedding:EasyNegative," + ] + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": [ + -969, + 484 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 2 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 768, + 512, + 1 + ] + }, + { + "id": 80, + "type": "Text box", + "pos": [ + -1397, + 356 + ], + "size": [ + 294, + 93.11674499511719 + ], + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 163 + ], + "shape": 3, + "slot_index": 0 + } + ], + "title": "❌Mel Negatives (general) (Negative)", + "properties": { + "Node name for S&R": "Text box" + }, + "widgets_values": [ + "embedding:EasyNegative, embedding:EasyNegativeV2, watermark, text, deformed" + ] + }, + { + "id": 72, + "type": "ConditioningAverage ", + "pos": [ + -960, + 2 + ], + "size": [ + 380.4000244140625, + 78 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "conditioning_to", + "type": "CONDITIONING", + "link": 151 + }, + { + "name": "conditioning_from", + "type": "CONDITIONING", + "link": 152, + "slot_index": 1 + }, + { + "name": "conditioning_to_strength", + "type": "FLOAT", + "link": 154, + "widget": { + "name": "conditioning_to_strength", + "config": [ + "FLOAT", + { + "default": 1, + "min": 0, + "max": 1, + "step": 0.01 + } + ] + }, + "slot_index": 2 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 153 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ConditioningAverage " + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + -1261, + -46 + ], + "size": [ + 210, + 54 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 28 + }, + { + "name": "text", + "type": "STRING", + "link": 145, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 151 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "A majestic Lion, fur in the wind, (smirk smile)" + ] + }, + { + "id": 70, + "type": "CLIPTextEncode", + "pos": [ + -1245, + 87 + ], + "size": [ + 210, + 54 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 149, + "slot_index": 0 + }, + { + "name": "text", + "type": "STRING", + "link": 150, + "widget": { + "name": "text", + "config": [ + "STRING", + { + "multiline": true + } + ] + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 152 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "A majestic Lion, fur in the wind, (smirk smile)" + ] + }, + { + "id": 69, + "type": "String Replace (mtb)", + "pos": [ + -1560, + -70 + ], + "size": [ + 210, + 82 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "string", + "type": "STRING", + "link": 141 + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 145 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String Replace (mtb)" + }, + "widgets_values": [ + "blue", + "yellow" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 68, + "type": "Text box", + "pos": [ + -1950, + 10 + ], + "size": [ + 217.2119140625, + 122.18632507324219 + ], + "flags": {}, + "order": 3, + "mode": 0, + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 141, + 150 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Text box" + }, + "widgets_values": [ + "A cinematic shot of a blue car, high speed, smoke whisps in the air, (at night), blue neon lighting" + ], + "color": "#432", + "bgcolor": "#653" + }, { "id": 3, "type": "KSampler", "pos": [ - -482.82979492187513, - -21.22597607421874 + -483, + -21 ], "size": [ 315, @@ -250,256 +657,6 @@ 1 ] }, - { - "id": 19, - "type": "CLIPSetLastLayer", - "pos": [ - -1597, - 209 - ], - "size": [ - 315, - 58 - ], - "flags": {}, - "order": 6, - "mode": 0, - "inputs": [ - { - "name": "clip", - "type": "CLIP", - "link": 116 - } - ], - "outputs": [ - { - "name": "CLIP", - "type": "CLIP", - "links": [ - 28, - 29, - 149 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "CLIPSetLastLayer" - }, - "widgets_values": [ - -2 - ] - }, - { - "id": 20, - "type": "Styles Loader (mtb)", - "pos": [ - -1615, - 404 - ], - "size": [ - 315, - 78 - ], - "flags": { - "collapsed": false - }, - "order": 1, - "mode": 0, - "outputs": [ - { - "name": "positive", - "type": "STRING", - "links": [], - "shape": 3 - }, - { - "name": "negative", - "type": "STRING", - "links": [ - 87 - ], - "shape": 3, - "slot_index": 1 - } - ], - "properties": { - "Node name for S&R": "Styles Loader (mtb)" - }, - "widgets_values": [ - "❌Mel Negatives (general)" - ] - }, - { - "id": 7, - "type": "CLIPTextEncode", - "pos": [ - -839, - 335 - ], - "size": [ - 210, - 54 - ], - "flags": {}, - "order": 11, - "mode": 0, - "inputs": [ - { - "name": "clip", - "type": "CLIP", - "link": 29 - }, - { - "name": "text", - "type": "STRING", - "link": 87, - "widget": { - "name": "text", - "config": [ - "STRING", - { - "multiline": true - } - ] - }, - "slot_index": 1 - } - ], - "outputs": [ - { - "name": "CONDITIONING", - "type": "CONDITIONING", - "links": [ - 6 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "CLIPTextEncode" - }, - "widgets_values": [ - "worst quality, hands, embedding:EasyNegative," - ] - }, - { - "id": 6, - "type": "CLIPTextEncode", - "pos": [ - -1263, - -42 - ], - "size": [ - 210, - 54 - ], - "flags": {}, - "order": 15, - "mode": 0, - "inputs": [ - { - "name": "clip", - "type": "CLIP", - "link": 28 - }, - { - "name": "text", - "type": "STRING", - "link": 145, - "widget": { - "name": "text", - "config": [ - "STRING", - { - "multiline": true - } - ] - }, - "slot_index": 1 - } - ], - "outputs": [ - { - "name": "CONDITIONING", - "type": "CONDITIONING", - "links": [ - 151 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "CLIPTextEncode" - }, - "widgets_values": [ - "A majestic Lion, fur in the wind, (smirk smile)" - ] - }, - { - "id": 72, - "type": "ConditioningAverage ", - "pos": [ - -988, - -13 - ], - "size": [ - 380.4000244140625, - 78 - ], - "flags": {}, - "order": 18, - "mode": 0, - "inputs": [ - { - "name": "conditioning_to", - "type": "CONDITIONING", - "link": 151 - }, - { - "name": "conditioning_from", - "type": "CONDITIONING", - "link": 152, - "slot_index": 1 - }, - { - "name": "conditioning_to_strength", - "type": "FLOAT", - "link": 154, - "widget": { - "name": "conditioning_to_strength", - "config": [ - "FLOAT", - { - "default": 1, - "min": 0, - "max": 1, - "step": 0.01 - } - ] - }, - "slot_index": 2 - } - ], - "outputs": [ - { - "name": "CONDITIONING", - "type": "CONDITIONING", - "links": [ - 153 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "ConditioningAverage " - }, - "widgets_values": [ - 1 - ] - }, { "id": 66, "type": "VAEDecodeTiled", @@ -548,8 +705,8 @@ "id": 78, "type": "SaveImage", "pos": [ - 566.1508954260959, - 5.399725291354571 + 565, + -18 ], "size": [ 315, @@ -571,103 +728,29 @@ ] }, { - "id": 5, - "type": "EmptyLatentImage", - "pos": [ - -969, - 484 - ], - "size": [ - 315, - 106 - ], - "flags": {}, - "order": 2, - "mode": 0, - "outputs": [ - { - "name": "LATENT", - "type": "LATENT", - "links": [ - 2 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "EmptyLatentImage" - }, - "widgets_values": [ - 768, - 512, - 1 - ] - }, - { - "id": 75, - "type": "PrimitiveNode", - "pos": [ - -1626, - 1228 - ], - "size": [ - 210, - 82 - ], - "flags": {}, - "order": 3, - "mode": 0, - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 156, - 159 - ], - "slot_index": 0, - "widget": { - "name": "total_frames", - "config": [ - "INT", - { - "default": 100, - "min": 0 - } - ] - } - } - ], - "properties": {}, - "widgets_values": [ - 24, - "fixed" - ] - }, - { - "id": 74, + "id": 81, "type": "Get Batch From History (mtb)", "pos": [ - -594, - 914 + -625, + 848 ], "size": [ - 315, - 102 + 235.1999969482422, + 118 ], "flags": {}, - "order": 14, + "order": 15, "mode": 0, "inputs": [ { - "name": "enable", - "type": "BOOL", - "link": 155 + "name": "passthrough_image", + "type": "IMAGE", + "link": null }, { "name": "count", "type": "INT", - "link": 159, + "link": 171, "widget": { "name": "count", "config": [ @@ -678,7 +761,22 @@ } ] }, - "slot_index": 1 + "slot_index": 2 + }, + { + "name": "enable", + "type": "BOOL", + "link": 168, + "widget": { + "name": "enable", + "config": [ + "BOOL", + { + "default": true + } + ] + }, + "slot_index": 2 } ], "outputs": [ @@ -686,8 +784,8 @@ "name": "i", "type": "IMAGE", "links": [ - 160, - 162 + 166, + 167 ], "shape": 3, "slot_index": 0 @@ -697,30 +795,30 @@ "Node name for S&R": "Get Batch From History (mtb)" }, "widgets_values": [ + true, 24, - 0, - false + 0 ] }, { "id": 77, "type": "Export To Prores (mtb)", "pos": [ - -57, - 911 + -111, + 849 ], "size": [ 315, 82 ], "flags": {}, - "order": 16, + "order": 17, "mode": 0, "inputs": [ { "name": "images", "type": "IMAGE", - "link": 160, + "link": 166, "slot_index": 0 } ], @@ -741,151 +839,60 @@ ] }, { - "id": 70, - "type": "CLIPTextEncode", + "id": 79, + "type": "Save Gif (mtb)", "pos": [ - -1246, - 72 + -104, + 1000 ], "size": [ 210, - 54 + 356 ], "flags": {}, - "order": 12, + "order": 18, "mode": 0, "inputs": [ { - "name": "clip", - "type": "CLIP", - "link": 149, - "slot_index": 0 + "name": "image", + "type": "IMAGE", + "link": 167 }, { - "name": "text", - "type": "STRING", - "link": 150, - "widget": { - "name": "text", - "config": [ - "STRING", - { - "multiline": true - } - ] - }, - "slot_index": 1 - } - ], - "outputs": [ - { - "name": "CONDITIONING", - "type": "CONDITIONING", - "links": [ - 152 - ], - "slot_index": 0 + "name": "pingpong", + "type": "BOOL", + "link": null } ], "properties": { - "Node name for S&R": "CLIPTextEncode" + "Node name for S&R": "Save Gif (mtb)" }, "widgets_values": [ - "A majestic Lion, fur in the wind, (smirk smile)" - ] - }, - { - "id": 68, - "type": "Text box", - "pos": [ - -1871, - 9 - ], - "size": [ - 217.2119140625, - 122.18632507324219 - ], - "flags": {}, - "order": 4, - "mode": 0, - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 141, - 150 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "Text box" - }, - "widgets_values": [ - "A cinematic shot of a blue car, high speed, blood trail" - ] - }, - { - "id": 69, - "type": "String Replace (mtb)", - "pos": [ - -1566, - -58 - ], - "size": [ - 210, - 82 - ], - "flags": {}, - "order": 9, - "mode": 0, - "inputs": [ - { - "name": "string", - "type": "STRING", - "link": 141 - } - ], - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 145 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "String Replace (mtb)" - }, - "widgets_values": [ - "blue", - "yellow" + 12, + 1, + true, + "/view?filename=03031ebda0.gif&subfolder=&type=output" ] }, { "id": 73, "type": "Animation Builder (mtb)", "pos": [ - -1260, - 850 + -1285, + 831 ], "size": [ 211.60000610351562, - 306 + 294 ], "flags": {}, - "order": 8, + "order": 9, "mode": 0, "inputs": [ { "name": "total_frames", "type": "INT", - "link": 156, + "link": 170, "widget": { "name": "total_frames", "config": [ @@ -895,7 +902,8 @@ "min": 0 } ] - } + }, + "slot_index": 0 } ], "outputs": [ @@ -925,7 +933,7 @@ "name": "loop_ended", "type": "BOOL", "links": [ - 155 + 168 ], "shape": 3, "slot_index": 3 @@ -938,47 +946,57 @@ 24, 1, 1, - 24, - 1, - "raw: 24\nframe: 0", - "Done 😎!", + 0, + 0, + "Idle", + "Iteration: Idle", "reset", "queue" - ] + ], + "color": "#232", + "bgcolor": "#353", + "shape": 1 }, { - "id": 79, - "type": "Save Gif (mtb)", + "id": 82, + "type": "PrimitiveNode", "pos": [ - -33, - 1041 + -1663, + 1205 ], "size": [ 210, - 338.00006103515625 + 82 ], "flags": {}, - "order": 17, + "order": 4, "mode": 0, - "inputs": [ + "outputs": [ { - "name": "image", - "type": "IMAGE", - "link": 162 - }, - { - "name": "pingpong", - "type": "BOOL", - "link": null + "name": "INT", + "type": "INT", + "links": [ + 170, + 171 + ], + "widget": { + "name": "total_frames", + "config": [ + "INT", + { + "default": 100, + "min": 0 + } + ] + }, + "slot_index": 0 } ], - "properties": { - "Node name for S&R": "Save Gif (mtb)" - }, + "title": "total_frames", + "properties": {}, "widgets_values": [ - 12, - 1, - true + 24, + "fixed" ] } ], @@ -1015,14 +1033,6 @@ 0, "CLIP" ], - [ - 87, - 20, - 1, - 7, - 1, - "STRING" - ], [ 116, 16, @@ -1151,38 +1161,6 @@ 2, "FLOAT" ], - [ - 155, - 73, - 3, - 74, - 0, - "BOOL" - ], - [ - 156, - 75, - 0, - 73, - 0, - "INT" - ], - [ - 159, - 75, - 0, - 74, - 1, - "INT" - ], - [ - 160, - 74, - 0, - 77, - 0, - "IMAGE" - ], [ 161, 66, @@ -1192,12 +1170,52 @@ "IMAGE" ], [ - 162, - 74, + 163, + 80, + 0, + 7, + 1, + "STRING" + ], + [ + 166, + 81, + 0, + 77, + 0, + "IMAGE" + ], + [ + 167, + 81, 0, 79, 0, "IMAGE" + ], + [ + 168, + 73, + 3, + 81, + 2, + "BOOL" + ], + [ + 170, + 82, + 0, + 73, + 0, + "INT" + ], + [ + 171, + 82, + 0, + 81, + 1, + "INT" ] ], "groups": [ diff --git a/examples/04-animation_builder-deforum.json b/examples/04-animation_builder-deforum.json index 0b66feb..99d3d0b 100644 --- a/examples/04-animation_builder-deforum.json +++ b/examples/04-animation_builder-deforum.json @@ -1,5 +1,5 @@ { - "last_node_id": 39, + "last_node_id": 42, "last_link_id": 69, "nodes": [ { @@ -93,7 +93,7 @@ ], "size": [ 210, - 36.36605696244669 + 36.366058349609375 ], "flags": {}, "order": 8, @@ -347,7 +347,7 @@ 114 ], "size": [ - 301.23302978515596, + 301.2330322265625, 98 ], "flags": {}, @@ -469,7 +469,7 @@ "Node name for S&R": "CLIPTextEncode" }, "widgets_values": [ - "A blonde doll with a pink dress sleeping, draping, wrinkles" + "A plastic Barbie doll with a pink dress sleeping, draping, wrinkles, shiny" ] }, { @@ -481,7 +481,7 @@ ], "size": [ 294, - 95.12843764123829 + 95.1284408569336 ], "flags": {}, "order": 3, @@ -502,7 +502,7 @@ "Node name for S&R": "Text box" }, "widgets_values": [ - "embedding:EasyNegative, embedding:EasyNegativeV2, watermark, text, deformed" + "embedding:EasyNegative, embedding:EasyNegativeV2, watermark, text, deformed, NSFW, Cleavage, Pubic Hair, Nudity, Naked, censored" ] }, { @@ -690,8 +690,8 @@ 259 ], "size": [ - 330.11123461913985, - 378.1239961059566 + 330.1112365722656, + 378.1239929199219 ], "flags": {}, "order": 19, @@ -716,8 +716,8 @@ 1256 ], "size": [ - 356.17758847656205, - 471.87858076171824 + 356.1775817871094, + 471.8785705566406 ], "flags": {}, "order": 14, @@ -735,8 +735,7 @@ "widgets_values": [ 12, 1, - true, - "/view?filename=7964e7cc8b.gif&subfolder=&type=output" + true ] }, { @@ -833,7 +832,7 @@ 882 ], "size": [ - 232.28509687500036, + 232.28509521484375, 166 ], "flags": {}, @@ -873,12 +872,12 @@ "id": 17, "type": "Animation Builder (mtb)", "pos": [ - -1309, + -1310, 883 ], "size": [ 211.60000610351562, - 294 + 318 ], "flags": {}, "order": 7, @@ -1175,8 +1174,8 @@ { "title": "START THE QUEUE BY CLICKLING HERE 👆", "bounding": [ - -1611, - 1196, + -1612, + 1219, 521, 80 ], From d143e83dba3bffa16e1b98d7ad1e9cf92dc94db2 Mon Sep 17 00:00:00 2001 From: melMass Date: Tue, 25 Jul 2023 00:29:39 +0200 Subject: [PATCH 67/70] =?UTF-8?q?fix:=20=E2=9C=A8=20Separate=20FaceAnalysi?= =?UTF-8?q?s=20model=20loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This closes #19 It is indeed much faster. --- examples/01-faceswap.json | 511 ++++++++++++++++++++------------------ nodes/faceswap.py | 60 +++-- 2 files changed, 320 insertions(+), 251 deletions(-) diff --git a/examples/01-faceswap.json b/examples/01-faceswap.json index 605697a..d3690c0 100644 --- a/examples/01-faceswap.json +++ b/examples/01-faceswap.json @@ -1,6 +1,6 @@ { - "last_node_id": 76, - "last_link_id": 161, + "last_node_id": 86, + "last_link_id": 178, "nodes": [ { "id": 59, @@ -14,7 +14,7 @@ 26 ], "flags": {}, - "order": 12, + "order": 13, "mode": 0, "inputs": [ { @@ -50,7 +50,7 @@ 26 ], "flags": {}, - "order": 8, + "order": 9, "mode": 0, "inputs": [ { @@ -85,7 +85,7 @@ 26 ], "flags": {}, - "order": 11, + "order": 12, "mode": 0, "inputs": [ { @@ -121,7 +121,7 @@ 26 ], "flags": {}, - "order": 6, + "order": 7, "mode": 0, "inputs": [ { @@ -167,7 +167,7 @@ { "name": "text", "type": "STRING", - "link": 87, + "link": 164, "widget": { "name": "text", "config": [ @@ -242,7 +242,7 @@ 164.31304931640625 ], "flags": {}, - "order": 7, + "order": 8, "mode": 0, "inputs": [ { @@ -316,6 +316,114 @@ "revAnimated_v122.safetensors" ] }, + { + "id": 25, + "type": "PreviewImage", + "pos": [ + 2338.9229387812507, + -632.8275743593749 + ], + "size": [ + 726.28564453125, + 475.3432312011719 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 150 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 75, + "type": "PreviewImage", + "pos": [ + 1573.5946044921875, + -629.80322265625 + ], + "size": [ + 691.7459716796875, + 479.6098327636719 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 169 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 73, + "type": "PreviewImage", + "pos": [ + 760, + -630 + ], + "size": [ + 671.1859741210938, + 483.1548156738281 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 153 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 79, + "type": "Text box", + "pos": [ + -1400, + 276 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 164 + ], + "shape": 3, + "slot_index": 0 + } + ], + "title": "❌Mel Negatives (general) (Negative)", + "properties": { + "Node name for S&R": "Text box" + }, + "widgets_values": [ + "embedding:EasyNegative, embedding:EasyNegativeV2, watermark, text, deformed, disfigured, blurry" + ] + }, { "id": 3, "type": "KSampler", @@ -328,7 +436,7 @@ 474 ], "flags": {}, - "order": 13, + "order": 14, "mode": 0, "inputs": [ { @@ -366,7 +474,7 @@ "Node name for S&R": "KSampler" }, "widgets_values": [ - 542821171532982, + 542821171533322, "fixed", 45, 8, @@ -389,7 +497,7 @@ "flags": { "collapsed": false }, - "order": 14, + "order": 15, "mode": 0, "inputs": [ { @@ -410,7 +518,7 @@ "type": "IMAGE", "links": [ 153, - 161 + 167 ], "shape": 3, "slot_index": 0 @@ -421,74 +529,24 @@ } }, { - "id": 25, - "type": "PreviewImage", + "id": 70, + "type": "Restore Face (mtb)", "pos": [ - 2338.9229387812507, - -632.8275743593749 + 2320, + -50 ], "size": [ - 726.28564453125, - 475.3432312011719 - ], - "flags": {}, - "order": 19, - "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 150 - } - ], - "properties": { - "Node name for S&R": "PreviewImage" - } - }, - { - "id": 75, - "type": "PreviewImage", - "pos": [ - 1573.5946044921875, - -629.80322265625 - ], - "size": [ - 691.7459716796875, - 479.6098327636719 + 315, + 186 ], "flags": {}, "order": 18, "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 156 - } - ], - "properties": { - "Node name for S&R": "PreviewImage" - } - }, - { - "id": 70, - "type": "Restore Face (mtb)", - "pos": [ - 1899, - -51 - ], - "size": [ - 315, - 150 - ], - "flags": {}, - "order": 17, - "mode": 0, "inputs": [ { "name": "image", "type": "IMAGE", - "link": 149 + "link": 168 }, { "name": "model", @@ -512,78 +570,56 @@ "Node name for S&R": "Restore Face (mtb)" }, "widgets_values": [ - "false", - "false", + false, + false, 0.5, - "true" + true ] }, { - "id": 67, - "type": "Face Swap (mtb)", + "id": 72, + "type": "UpscaleModelLoader", "pos": [ - 1127, - -26 + 1715, + 52 ], "size": [ - 315, - 122 + 260.3902351585391, + 58 ], "flags": {}, - "order": 16, + "order": 3, "mode": 0, - "inputs": [ - { - "name": "image", - "type": "IMAGE", - "link": 161 - }, - { - "name": "reference", - "type": "IMAGE", - "link": 160, - "slot_index": 1 - }, - { - "name": "faceswap_model", - "type": "FACESWAP_MODEL", - "link": 147, - "slot_index": 2 - } - ], "outputs": [ { - "name": "IMAGE", - "type": "IMAGE", + "name": "UPSCALE_MODEL", + "type": "UPSCALE_MODEL", "links": [ - 149, - 156 + 152 ], - "shape": 3, - "slot_index": 0 + "shape": 3 } ], "properties": { - "Node name for S&R": "Face Swap (mtb)" + "Node name for S&R": "UpscaleModelLoader" }, "widgets_values": [ - "0", - "false" + "4x-UltraSharp.pth" ] }, { "id": 71, "type": "Load Face Enhance Model (mtb)", "pos": [ - 1476, - 240 + 2005, + 40 ], "size": [ - 315, - 82 + 265.97600515853924, + 87.31192548828142 ], "flags": {}, - "order": 9, + "order": 11, "mode": 0, "inputs": [ { @@ -611,82 +647,26 @@ 2 ] }, - { - "id": 72, - "type": "UpscaleModelLoader", - "pos": [ - 1110, - 241 - ], - "size": [ - 315, - 58 - ], - "flags": {}, - "order": 2, - "mode": 0, - "outputs": [ - { - "name": "UPSCALE_MODEL", - "type": "UPSCALE_MODEL", - "links": [ - 152 - ], - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "UpscaleModelLoader" - }, - "widgets_values": [ - "4x-UltraSharp.pth" - ] - }, - { - "id": 73, - "type": "PreviewImage", - "pos": [ - 760, - -630 - ], - "size": [ - 671.1859741210938, - 483.1548156738281 - ], - "flags": {}, - "order": 15, - "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 153 - } - ], - "properties": { - "Node name for S&R": "PreviewImage" - } - }, { "id": 76, "type": "Load Image From Url (mtb)", "pos": [ - 161, - 231 + 624, + 39 ], "size": [ 315, 58 ], "flags": {}, - "order": 3, + "order": 4, "mode": 0, "outputs": [ { "name": "IMAGE", "type": "IMAGE", "links": [ - 160 + 166 ], "shape": 3, "slot_index": 0 @@ -703,22 +683,22 @@ "id": 69, "type": "Load Face Swap Model (mtb)", "pos": [ - 172, - 503 + 621, + 252 ], "size": [ 315, 58 ], "flags": {}, - "order": 4, + "order": 5, "mode": 0, "outputs": [ { "name": "FACESWAP_MODEL", "type": "FACESWAP_MODEL", "links": [ - 147 + 165 ], "shape": 3 } @@ -731,43 +711,92 @@ ] }, { - "id": 20, - "type": "Styles Loader (mtb)", + "id": 81, + "type": "Load Face Analysis Model (mtb)", "pos": [ - -1341, - 276 + 624, + 144 ], "size": [ 315, - 78 + 58 ], - "flags": { - "collapsed": false - }, - "order": 5, + "flags": {}, + "order": 6, "mode": 0, "outputs": [ { - "name": "positive", - "type": "STRING", - "links": [], - "shape": 3 - }, - { - "name": "negative", - "type": "STRING", + "name": "FACE_ANALYSIS_MODEL", + "type": "FACE_ANALYSIS_MODEL", "links": [ - 87 + 170 ], - "shape": 3, - "slot_index": 1 + "shape": 3 } ], "properties": { - "Node name for S&R": "Styles Loader (mtb)" + "Node name for S&R": "Load Face Analysis Model (mtb)" }, "widgets_values": [ - "❌Low Token" + "buffalo_l" + ] + }, + { + "id": 80, + "type": "Face Swap (mtb)", + "pos": [ + 1255, + -32 + ], + "size": [ + 210, + 154 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 167 + }, + { + "name": "reference", + "type": "IMAGE", + "link": 166 + }, + { + "name": "faceanalysis_model", + "type": "FACE_ANALYSIS_MODEL", + "link": 170, + "slot_index": 2 + }, + { + "name": "faceswap_model", + "type": "FACESWAP_MODEL", + "link": 165, + "slot_index": 3 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 168, + 169 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "Face Swap (mtb)" + }, + "widgets_values": [ + "0", + false ] } ], @@ -796,14 +825,6 @@ 2, "CONDITIONING" ], - [ - 87, - 20, - 1, - 7, - 1, - "STRING" - ], [ 117, 16, @@ -876,22 +897,6 @@ 0, "CLIP" ], - [ - 147, - 69, - 0, - 67, - 2, - "FACESWAP_MODEL" - ], - [ - 149, - 67, - 0, - 70, - 0, - "IMAGE" - ], [ 150, 70, @@ -925,28 +930,60 @@ "IMAGE" ], [ - 156, - 67, + 164, + 79, + 0, + 7, + 1, + "STRING" + ], + [ + 165, + 69, + 0, + 80, + 3, + "FACESWAP_MODEL" + ], + [ + 166, + 76, + 0, + 80, + 1, + "IMAGE" + ], + [ + 167, + 66, + 0, + 80, + 0, + "IMAGE" + ], + [ + 168, + 80, + 0, + 70, + 0, + "IMAGE" + ], + [ + 169, + 80, 0, 75, 0, "IMAGE" ], [ - 160, - 76, + 170, + 81, 0, - 67, - 1, - "IMAGE" - ], - [ - 161, - 66, - 0, - 67, - 0, - "IMAGE" + 80, + 2, + "FACE_ANALYSIS_MODEL" ] ], "groups": [ diff --git a/nodes/faceswap.py b/nodes/faceswap.py index c7d8f70..bf8c2db 100644 --- a/nodes/faceswap.py +++ b/nodes/faceswap.py @@ -22,6 +22,37 @@ import comfy.model_management as model_management log = mklog(__name__) +class LoadFaceAnalysisModel: + """Loads a face analysis model""" + + models = [] + @staticmethod + def get_models() -> List[str]: + models_path = os.path.join(folder_paths.models_dir, "insightface/*") + models = glob.glob(models_path) + models = [Path(x).name for x in models if x.endswith(".onnx") or x.endswith(".pth")] + return models + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "faceswap_model": ( + ["antelopev2", "buffalo_l", "buffalo_m", "buffalo_sc"], + {"default": "buffalo_l"}, + ), + }, + } + + RETURN_TYPES = ("FACE_ANALYSIS_MODEL",) + FUNCTION = "load_model" + CATEGORY = "mtb/facetools" + + def load_model(self, faceswap_model: str): + face_analyser = insightface.app.FaceAnalysis( + name=faceswap_model, root=os.path.join(folder_paths.models_dir, "insightface") + ) + return (face_analyser,) class LoadFaceSwapModel: """Loads a faceswap model""" @@ -81,6 +112,7 @@ class FaceSwap: "image": ("IMAGE",), "reference": ("IMAGE",), "faces_index": ("STRING", {"default": "0"}), + "faceanalysis_model": ("FACE_ANALYSIS_MODEL", {"default": "None"}), "faceswap_model": ("FACESWAP_MODEL", {"default": "None"}), "debug": ("BOOL", {"default": False}), }, @@ -96,6 +128,7 @@ class FaceSwap: image: torch.Tensor, reference: torch.Tensor, faces_index: str, + faceanalysis_model, faceswap_model, debug=False, ): @@ -107,7 +140,7 @@ class FaceSwap: int(x) for x in faces_index.strip(",").split(",") if x.isnumeric() } sys.stdout = NullWriter() - swapped = swap_face(ref, img, faceswap_model, face_ids) + swapped = swap_face(faceanalysis_model,ref, img, faceswap_model, face_ids) sys.stdout = sys.__stdout__ return pil2tensor(swapped) @@ -121,8 +154,8 @@ class FaceSwap: image = do_swap(image) else: - image = [do_swap(image[i]) for i in range(batch_count)] - image = torch.cat(image, dim=0) + image_batch = [do_swap(image[i]) for i in range(batch_count)] + image = torch.cat(image_batch, dim=0) return (image,) @@ -131,17 +164,15 @@ class FaceSwap: # region face swap utils -def get_face_single(img_data: np.ndarray, face_index=0, det_size=(640, 640)): - face_analyser = insightface.app.FaceAnalysis( - name="buffalo_l", root=os.path.join(folder_paths.models_dir, "insightface") - ) +def get_face_single(face_analyser,img_data: np.ndarray, face_index=0, det_size=(640, 640)): + face_analyser.prepare(ctx_id=0, det_size=det_size) face = face_analyser.get(img_data) if len(face) == 0 and det_size[0] > 320 and det_size[1] > 320: log.debug("No face ed, trying again with smaller image") det_size_half = (det_size[0] // 2, det_size[1] // 2) - return get_face_single(img_data, face_index=face_index, det_size=det_size_half) + return get_face_single(face_analyser,img_data, face_index=face_index, det_size=det_size_half) try: return sorted(face, key=lambda x: x.bbox[0])[face_index] @@ -150,6 +181,7 @@ def get_face_single(img_data: np.ndarray, face_index=0, det_size=(640, 640)): def swap_face( + face_analyser, source_img: Union[Image.Image, List[Image.Image]], target_img: Union[Image.Image, List[Image.Image]], face_swapper_model, @@ -161,14 +193,14 @@ def swap_face( result_image = target_img if face_swapper_model is not None: - source_img = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR) - target_img = cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR) - source_face = get_face_single(source_img, face_index=0) + cv_source_img = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR) + cv_target_img = cv2.cvtColor(np.array(target_img), cv2.COLOR_RGB2BGR) + source_face = get_face_single(face_analyser,cv_source_img, face_index=0) if source_face is not None: - result = target_img + result = cv_target_img for face_num in faces_index: - target_face = get_face_single(target_img, face_index=face_num) + target_face = get_face_single(face_analyser,cv_target_img, face_index=face_num) if target_face is not None: sys.stdout = NullWriter() result = face_swapper_model.get(result, target_face, source_face) @@ -187,4 +219,4 @@ def swap_face( # endregion face swap utils -__nodes__ = [FaceSwap, LoadFaceSwapModel] +__nodes__ = [FaceSwap, LoadFaceSwapModel, LoadFaceAnalysisModel] From 96a0618c5990a8559a9e2dd17c868d3465b8ca90 Mon Sep 17 00:00:00 2001 From: melMass Date: Tue, 25 Jul 2023 00:43:21 +0200 Subject: [PATCH 68/70] =?UTF-8?q?docs:=20=F0=9F=93=9D=20update=20readmes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-CN.md | 29 +++++++++++++---------------- README-JP.md | 31 +++++++++++++------------------ README.md | 32 ++++++++++++++------------------ 3 files changed, 40 insertions(+), 52 deletions(-) diff --git a/README-CN.md b/README-CN.md index 106040c..8b796e6 100644 --- a/README-CN.md +++ b/README-CN.md @@ -1,13 +1,13 @@ -## MTB Nodes +# MTB Nodes Buy Me A Coffee +[** 安装指南**](./INSTALL.md) | [** 示例**](https://github.com/melMass/comfy_mtb/wiki/Examples) + 欢迎使用 MTB Nodes 项目!这个代码库是开放的,您可以自由地探索和利用。它的主要目的是构建用于 [MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs) 中的概念验证(POCs)。该项目中的许多节点都是受到现有社区贡献或内置功能的启发而创建的。 在继续之前,请注意与此项目中使用的某些库相关的许可证。例如,`deepbump` 库采用 [GPLv3](https://github.com/HugoTini/DeepBump/blob/master/LICENSE) 许可证。 -- [MTB Nodes](#mtb-nodes) -- [安装](#安装) - [节点列表](#节点列表) - [bbox](#bbox) - [colors](#colors) @@ -20,27 +20,24 @@ - [Comfy 资源](#comfy-资源) -## 安装 - -- 移至 [INSTALL-CN.md](./INSTALL-CN.md) -## 节点列表 +# 节点列表 -### bbox +## bbox - `Bounding Box`: BBox 构造函数(自定义类型) - `BBox From Mask`: 从遮罩中提取边界框 - `Crop`: 根据边界框裁剪图像 - `Uncrop`: 根据边界框还原图像 -### colors +## colors - `Colored Image`: 给定尺寸的纯色图像 - `RGB to HSV`: - - `HSV to RGB`: - - `Color Correct`: 基本颜色校正工具 -### 人脸检测/交换 +## 人脸检测/交换 - `Face Swap`: 使用 deepinsight/insightface 模型进行人脸交换(该节点在早期版本中称为 `Roop`,功能相同,`Roop` 只是使用这些模型的应用程序) > **注意** > 人脸索引允许您选择要替换的人脸,如下所示: @@ -48,13 +45,13 @@ - `Load Face Swap Model`: 加载 insightface 模型用于人脸交换 - `Restore Face`: 使用 [GFPGan](https://github.com/TencentARC/GFPGAN) 还原人脸,与 `Face Swap` 配合使用效果很好,并支持 `bg_upscaler` 的 Comfy 原生放大器 -### 图像插值(动画) +## 图像插值(动画) - `Load Film Model`: 加载 [FILM](https://github.com/google-research/frame-interpolation) 模型 - `Film Interpolation`: 使用 [FILM](https://github.com/google-research/frame-interpolation) 处理输入帧 - `Export to Prores (experimental)`: 将输入帧导出为 ProRes 4444 mov 文件。这使用 ffmpeg stdin 发送原始的 NumPy 数组,与 `Film Interpolation` 一起使用,目前很简单,但可以进一步扩展。 -### 图像操作 +## 图像操作 - `Blur`: 使用高斯滤波器对图像进行模糊处理。 - `Deglaze Image`: 从 [FN16](https://github.com/Fannovel16/FN16-ComfyUI-nodes/blob/main/DeglazeImage.py) 中提取 - `Denoise`: 对输入图像进行降噪处理 @@ -66,11 +63,11 @@ - `Mask To Image`: 将遮罩(Alpha)转换为带有颜色和背景的 RGB 图像 - `Save Image Grid`: 将输入批次中的所有图像保存为图像网格。 -### 潜在变量工具 +## 潜在变量工具 - `Latent Lerp`: 两个潜在变量之间的线性插值(混合) -### 其他工具 +## 其他工具 - `Concat Images`: 接受两个图像流,并将它们合并为其他 Comfy 管道支持的图像批次。 - `Image Resize Factor`: **已弃用**,因为我后来发现了内 @@ -84,11 +81,11 @@ - `Int to Number`: 用于 WASSuite 数字节点的补充 - `Smart Step`: 使用百分比来控制 `KAdvancedSampler` 的步骤(开始/停止) -### 纹理 +## 纹理 - `DeepBump`: 从单张图片生成法线图和高度图 -## Comfy 资源 +# Comfy 资源 **指南**: - [官方示例(英文)](https://comfyanonymous.github.io/ComfyUI_examples/) diff --git a/README-JP.md b/README-JP.md index a48b5ef..4d21024 100644 --- a/README-JP.md +++ b/README-JP.md @@ -1,13 +1,13 @@ -## MTB Nodes +# MTB Nodes Buy Me A Coffee +[**インストールガイド**](./INSTALL.md) | [**サンプル**](https://github.com/melMass/comfy_mtb/wiki/Examples) + MTB Nodesプロジェクトへようこそ!このコードベースは、自由に探索し、利用することができます。主な目的は、[MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs)の実装のための概念実証(POC)を構築することです。このプロジェクトの多くのノードは、既存のコミュニティの貢献や組み込みの機能に触発されています。 続行する前に、このプロジェクトで使用されている特定のライブラリに関連するライセンスに注意してください。たとえば、「deepbump」ライブラリは、[GPLv3](https://github.com/HugoTini/DeepBump/blob/master/LICENSE)の下でライセンスされています。 -- [MTB Nodes](#mtb-nodes) -- [インストール](#インストール) - [ノードリスト](#ノードリスト) - [bbox](#bbox) - [colors](#colors) @@ -20,27 +20,22 @@ MTB Nodesプロジェクトへようこそ!このコードベースは、自 - [Comfyリソース](#comfyリソース) -## インストール +# ノードリスト -- [INSTALL-JP.md](./INSTALL-JP.md)に移動しました。 - - -## ノードリスト - -### bbox +## bbox - `Bounding Box`: BBoxコンストラクタ(カスタムタイプ) - `BBox From Mask`: マスクからバウンディングボックスを抽出 - `Crop`: BBoxから画像を切り抜く - `Uncrop`: BBoxから画像を元に戻す -### colors +## colors - `Colored Image`: 指定されたサイズの一定の色の画像 - `RGB to HSV`: - - `HSV to RGB`: - - `Color Correct`: 基本的なカラーコレクションツール -### 顔検出 / スワッピング +## 顔検出 / スワッピング - `Face Swap`: deepinsight/insightfaceモデルを使用した顔の入れ替え(このノードは初期バージョンでは「Roop」と呼ばれていましたが、同じ機能を提供します。Roopは単にこれらのモデルを使用するアプリです) > **注意** > 顔のインデックスを使用して置き換える顔を選択できます。以下を参照してください: @@ -48,13 +43,13 @@ MTB Nodesプロジェクトへようこそ!このコードベースは、自 - `Load Face Swap Model`: 顔の交換のためのinsightfaceモデルを読み込む - `Restore Face`: [GFPGan](https://github.com/TencentARC/GFPGAN)を使用して顔を復元し、`Face Swap`と組み合わせて使用すると非常に効果的であり、`bg_upscaler`のComfyネイティブアップスケーラーもサポートしています。 -### 画像補間(アニメーション) +## 画像補間(アニメーション) - `Load Film Model`: [FILM](https://github.com/google-research/frame-interpolation)モデルを読み込む - `Film Interpolation`: [FILM](https://github.com/google-research/frame-interpolation)を使用して入力フレームを処理する - `Export to Prores (experimental)`: 入力フレームをProRes 4444 movファイルにエクスポートします。これは現在は単純なものですが、`Film Interpolation`と組み合わせて使用するためのffmpegのstdinを使用して生のNumPy配列を送信するもので、拡張することもできます。 -### 画像操作 +## 画像操作 - `Blur`: ガウスフィルタを使用して画像をぼかす - `Deglaze Image`: [FN16](https://github.com/Fannovel16/FN16-ComfyUI-nodes/blob/main/DeglazeImage.py)から取得 - `Denoise`: 入力画像のノイズを除去する @@ -68,10 +63,10 @@ MTB Nodesプロジェクトへようこそ!このコードベースは、自 - `Mask To Image`: マスク(アルファ)をカラーと背景を持つRGBイメージに変換します。 - `Save Image Grid`: 入力バッチのすべての画像を画像グリッドとして保存します。 -### 潜在的なユーティリティ +## 潜在的なユーティリティ - `Latent Lerp`: 2つの潜在的なベクトルの間の線形補間(ブレンド) -### その他のユーティリティ +## その他のユーティリティ - `Concat Images`: 2つの画像ストリームを取り、他のComfyパイプラインでサポートされている画像のバッチとしてマージします。 - `Image Resize Factor`: **非推奨**。組み込みの画像リサイズ機能を発見したため、削除される予定です。 - `Text To Image`: フォントを使用してテキストを画像に変換するためのユーティリティ @@ -83,11 +78,11 @@ MTB Nodesプロジェクトへようこそ!このコードベースは、自 - `Int to Number`: WASSuiteの数値ノードの補完 - `Smart Step`: `KAdvancedSampler`のステップ(開始/停止)を制御するための非常に基本的なツールで、パーセンテージを使用します。 -### テクスチャ +## テクスチャ - `DeepBump`: 1枚の画像から法線マップと高さマップを生成します。 -## Comfyリソース +# Comfyリソース **ガイド**: - [公式の例(英語)](https://comfyanonymous.github.io/ComfyUI_examples/) diff --git a/README.md b/README.md index 6b8567a..2dc6e55 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -## MTB Nodes +# MTB Nodes + **Translated Readme (using DeepTranslate, PRs are welcome)**: ![image](https://github.com/melMass/comfy_mtb/assets/7041726/f8429c14-3521-4e28-82a3-863d781976c0) @@ -8,12 +9,12 @@ Buy Me A Coffee +[**Install Guide**](./INSTALL.md) | [**Examples**](https://github.com/melMass/comfy_mtb/wiki/Examples) + Welcome to the MTB Nodes project! This codebase is open for you to explore and utilize as you wish. Its primary purpose is to build proof-of-concepts (POCs) for implementation in [MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs). Many nodes in this project are inspired by existing community contributions or built-in functionalities. Before proceeding, please be aware of the licenses associated with certain libraries used in this project. For example, the `deepbump` library is licensed under [GPLv3](https://github.com/HugoTini/DeepBump/blob/master/LICENSE). -- [MTB Nodes](#mtb-nodes) -- [Installation](#installation) - [Node List](#node-list) - [bbox](#bbox) - [colors](#colors) @@ -26,27 +27,22 @@ Before proceeding, please be aware of the licenses associated with certain libra - [Comfy Resources](#comfy-resources) -## Installation +# Node List -- Moved to [INSTALL.md](./INSTALL.md) - - -## Node List - -### bbox +## bbox - `Bounding Box`: BBox constructor (custom type), - `BBox From Mask`: From a mask extract the bounding box - `Crop`: Crop image from BBox - `Uncrop`: Uncrop image from BBox -### colors +## colors - `Colored Image`: Constant color image of given size - `RGB to HSV`: -, - `HSV to RGB`: -, - `Color Correct`: Basic color correction tools -### face detection / swapping +## face detection / swapping - `Face Swap`: Face swap using deepinsight/insightface models (this node used to be called `Roop` in early versions, it does the same, roop is *just* an app that uses those model) > **Note** > The face index allow you to choose which face to replace as you can see here: @@ -54,13 +50,13 @@ Before proceeding, please be aware of the licenses associated with certain libra - `Load Face Swap Model`: Load an insightface model for face swapping - `Restore Face`: Using [GFPGan](https://github.com/TencentARC/GFPGAN) to restore faces, works great in conjunction with `Face Swap` and supports Comfy native upscalers for the `bg_upscaler` -### image interpolation (animation) +## image interpolation (animation) - `Load Film Model`: Loads a [FILM](https://github.com/google-research/frame-interpolation) model - `Film Interpolation`: Process input frames using [FILM](https://github.com/google-research/frame-interpolation) - `Export to Prores (experimental)`: Exports the input frames to a ProRes 4444 mov file. This is using ffmpeg stdin to send raw numpy arrays, used with `Film Interpolation` and very simple for now but could be expanded upon. -### image ops +## image ops - `Blur`: Blur an image using a Gaussian filter. - `Deglaze Image`: taken from [FN16](https://github.com/Fannovel16/FN16-ComfyUI-nodes/blob/main/DeglazeImage.py), - `Denoise`: Denoise the input image, @@ -72,11 +68,11 @@ Before proceeding, please be aware of the licenses associated with certain libra - `Mask To Image`: Converts a mask (alpha) to an RGB image with a color and background - `Save Image Grid`: Save all the images in the input batch as a grid of images. -### latent utils +## latent utils - `Latent Lerp`: Linear interpolation (blend) between two latent -### misc utils +## misc utils - `Concat Images`: Takes two image stream and merge them as a batch of images supported by other Comfy pipelines. - `Image Resize Factor`: **Deprecated**, I since discovered the builtin image resize. - `Text To Image`: Utils to convert text to image using a font @@ -88,11 +84,11 @@ Before proceeding, please be aware of the licenses associated with certain libra - `Int to Number`: Supplement for WASSuite number nodes - `Smart Step`: A very basic tool to control the steps (start/stop) of the `KAdvancedSampler` using percentage -### textures +## textures - `DeepBump`: Normal & height maps generation from single pictures -## Comfy Resources +# Comfy Resources **Guides**: - [Official Examples (eng)](https://comfyanonymous.github.io/ComfyUI_examples/) From 232cf8966cc20291b60c68f487dfd37bf6aa4dfa Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Tue, 25 Jul 2023 02:05:57 +0200 Subject: [PATCH 69/70] =?UTF-8?q?docs:=20=F0=9F=93=9D=20link=20to=20the=20?= =?UTF-8?q?proper=20lang=20instructions=20(#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-CN.md | 4 ++-- README-JP.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README-CN.md b/README-CN.md index 8b796e6..7300895 100644 --- a/README-CN.md +++ b/README-CN.md @@ -2,7 +2,7 @@ Buy Me A Coffee -[** 安装指南**](./INSTALL.md) | [** 示例**](https://github.com/melMass/comfy_mtb/wiki/Examples) +[** 安装指南**](./INSTALL-CN.md) | [** 示例**](https://github.com/melMass/comfy_mtb/wiki/Examples) 欢迎使用 MTB Nodes 项目!这个代码库是开放的,您可以自由地探索和利用。它的主要目的是构建用于 [MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs) 中的概念验证(POCs)。该项目中的许多节点都是受到现有社区贡献或内置功能的启发而创建的。 @@ -96,4 +96,4 @@ **扩展和自定义节点**: - @WASasquatch 的[Comfy 列表插件(英文)](https://github.com/WASasquatch/comfyui-plugins) -- [CivitAI 上的 ComfyUI 标签(英文)](https://civitai.com/tag/comfyui) \ No newline at end of file +- [CivitAI 上的 ComfyUI 标签(英文)](https://civitai.com/tag/comfyui) diff --git a/README-JP.md b/README-JP.md index 4d21024..308bb87 100644 --- a/README-JP.md +++ b/README-JP.md @@ -2,7 +2,7 @@ Buy Me A Coffee -[**インストールガイド**](./INSTALL.md) | [**サンプル**](https://github.com/melMass/comfy_mtb/wiki/Examples) +[**インストールガイド**](./INSTALL-JP.md) | [**サンプル**](https://github.com/melMass/comfy_mtb/wiki/Examples) MTB Nodesプロジェクトへようこそ!このコードベースは、自由に探索し、利用することができます。主な目的は、[MLOPs](https://github.com/Bismuth-Consultancy-BV/MLOPs)の実装のための概念実証(POC)を構築することです。このプロジェクトの多くのノードは、既存のコミュニティの貢献や組み込みの機能に触発されています。 @@ -93,4 +93,4 @@ MTB Nodesプロジェクトへようこそ!このコードベースは、自 **拡張機能とカスタムノード**: - @WASasquatchによる[Comfyリスト用のプラグイン(英語)](https://github.com/WASasquatch/comfyui-plugins) -- [CivitAIのComfyUIタグ(英語)](https://civitai.com/tag/comfyui) \ No newline at end of file +- [CivitAIのComfyUIタグ(英語)](https://civitai.com/tag/comfyui) From c2377374201fc34b107c8b7db1cdeb2f483d1e18 Mon Sep 17 00:00:00 2001 From: melMass Date: Tue, 25 Jul 2023 02:30:37 +0200 Subject: [PATCH 70/70] =?UTF-8?q?chore:=20=F0=9F=91=B7=20remove=20stale=20?= =?UTF-8?q?example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/advanced-comfyforum.json | 932 ------------------------------ 1 file changed, 932 deletions(-) delete mode 100644 examples/advanced-comfyforum.json diff --git a/examples/advanced-comfyforum.json b/examples/advanced-comfyforum.json deleted file mode 100644 index adc807f..0000000 --- a/examples/advanced-comfyforum.json +++ /dev/null @@ -1,932 +0,0 @@ -{ - "last_node_id": 24, - "last_link_id": 30, - "nodes": [ - { - "id": 11, - "type": "Get Batch From History (mtb)", - "pos": [ - -828, - 522 - ], - "size": [ - 235.1999969482422, - 118 - ], - "flags": {}, - "order": 12, - "mode": 0, - "inputs": [ - { - "name": "passthrough_image", - "type": "IMAGE", - "link": 10 - }, - { - "name": "enable", - "type": "BOOL", - "link": 9, - "widget": { - "name": "enable", - "config": [ - "BOOL", - { - "default": true - } - ] - }, - "slot_index": 1 - } - ], - "outputs": [ - { - "name": "i", - "type": "IMAGE", - "links": [ - 26 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "Get Batch From History (mtb)" - }, - "widgets_values": [ - false, - 1, - 0 - ], - "color": "#223", - "bgcolor": "#335" - }, - { - "id": 24, - "type": "Note", - "pos": [ - -827, - 406 - ], - "size": [ - 229.35427033319013, - 69.73426380916959 - ], - "flags": {}, - "order": 5, - "mode": 0, - "properties": { - "text": "" - }, - "widgets_values": [ - "On first frame we get the init image, on all subsequent ones the feedback from the previous queue item" - ], - "color": "#223", - "bgcolor": "#335", - "shape": 1 - }, - { - "id": 10, - "type": "LoadImage", - "pos": [ - -1388, - 321 - ], - "size": [ - 315, - 314 - ], - "flags": {}, - "order": 4, - "mode": 0, - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 10 - ], - "shape": 3, - "slot_index": 0 - }, - { - "name": "MASK", - "type": "MASK", - "links": null, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "LoadImage" - }, - "widgets_values": [ - "example.png", - "image" - ] - }, - { - "id": 13, - "type": "PrimitiveNode", - "pos": [ - -1679, - 817 - ], - "size": [ - 210, - 82 - ], - "flags": {}, - "order": 3, - "mode": 0, - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 8 - ], - "widget": { - "name": "int", - "config": [ - "INT", - { - "default": 0 - } - ] - } - } - ], - "title": "int", - "properties": {}, - "widgets_values": [ - 0, - "increment" - ] - }, - { - "id": 12, - "type": "Int To Bool (mtb)", - "pos": [ - -1290, - 818 - ], - "size": [ - 210, - 42.2801628112793 - ], - "flags": {}, - "order": 9, - "mode": 0, - "inputs": [ - { - "name": "int", - "type": "INT", - "link": 8, - "widget": { - "name": "int", - "config": [ - "INT", - { - "default": 0 - } - ] - }, - "slot_index": 0 - } - ], - "outputs": [ - { - "name": "BOOL", - "type": "BOOL", - "links": [ - 9 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "Int To Bool (mtb)" - }, - "widgets_values": [ - 0 - ] - }, - { - "id": 14, - "type": "Transform Image (mtb)", - "pos": [ - -54, - 515 - ], - "size": [ - 315, - 154 - ], - "flags": {}, - "order": 13, - "mode": 0, - "inputs": [ - { - "name": "image", - "type": "IMAGE", - "link": 26 - }, - { - "name": "angle", - "type": "FLOAT", - "link": 30, - "widget": { - "name": "angle", - "config": [ - "FLOAT", - { - "default": 0 - } - ] - } - } - ], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 24 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "Transform Image (mtb)" - }, - "widgets_values": [ - 0, - 5, - 1.02, - 0, - 0 - ] - }, - { - "id": 20, - "type": "Export To Prores (mtb)", - "pos": [ - 394, - 1803 - ], - "size": [ - 315, - 82 - ], - "flags": {}, - "order": 10, - "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 17 - } - ], - "outputs": [ - { - "name": "VIDEO", - "type": "VIDEO", - "links": null, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "Export To Prores (mtb)" - }, - "widgets_values": [ - 12, - "export" - ] - }, - { - "id": 19, - "type": "Save Gif (mtb)", - "pos": [ - 392, - 1321 - ], - "size": [ - 210, - 336 - ], - "flags": {}, - "order": 11, - "mode": 0, - "inputs": [ - { - "name": "image", - "type": "IMAGE", - "link": 18 - } - ], - "properties": { - "Node name for S&R": "Save Gif (mtb)" - }, - "widgets_values": [ - 12, - 1, - true, - "/view?filename=8b62123f3c.gif&subfolder=&type=output" - ] - }, - { - "id": 16, - "type": "Styles Loader (mtb)", - "pos": [ - 72, - 213 - ], - "size": [ - 315, - 78 - ], - "flags": {}, - "order": 1, - "mode": 0, - "outputs": [ - { - "name": "positive", - "type": "STRING", - "links": null, - "shape": 3 - }, - { - "name": "negative", - "type": "STRING", - "links": [ - 15 - ], - "shape": 3, - "slot_index": 1 - } - ], - "properties": { - "Node name for S&R": "Styles Loader (mtb)" - }, - "widgets_values": [ - "❌Low Token" - ] - }, - { - "id": 9, - "type": "CheckpointLoaderSimple", - "pos": [ - 64, - 68 - ], - "size": [ - 315, - 98 - ], - "flags": {}, - "order": 0, - "mode": 0, - "outputs": [ - { - "name": "MODEL", - "type": "MODEL", - "links": [ - 4 - ], - "shape": 3 - }, - { - "name": "CLIP", - "type": "CLIP", - "links": [ - 5 - ], - "shape": 3, - "slot_index": 1 - }, - { - "name": "VAE", - "type": "VAE", - "links": [ - 6 - ], - "shape": 3, - "slot_index": 2 - } - ], - "properties": { - "Node name for S&R": "CheckpointLoaderSimple" - }, - "widgets_values": [ - "revAnimated_v122.safetensors" - ] - }, - { - "id": 22, - "type": "Fit Number (mtb)", - "pos": [ - 39, - 842 - ], - "size": [ - 210, - 166 - ], - "flags": {}, - "order": 6, - "mode": 0, - "inputs": [ - { - "name": "value", - "type": "FLOAT", - "link": 27 - } - ], - "outputs": [ - { - "name": "FLOAT", - "type": "FLOAT", - "links": [ - 28 - ], - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "Fit Number (mtb)" - }, - "widgets_values": [ - true, - 0, - 1, - 0.9, - 0.2 - ] - }, - { - "id": 23, - "type": "Fit Number (mtb)", - "pos": [ - -342, - 621 - ], - "size": [ - 210, - 166 - ], - "flags": {}, - "order": 7, - "mode": 0, - "inputs": [ - { - "name": "value", - "type": "FLOAT", - "link": 29 - } - ], - "outputs": [ - { - "name": "FLOAT", - "type": "FLOAT", - "links": [ - 30 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "Fit Number (mtb)" - }, - "widgets_values": [ - true, - 0.7, - 1, - 0, - 3 - ] - }, - { - "id": 8, - "type": "## i2i [cac944]", - "pos": [ - 567, - 65 - ], - "size": [ - 400, - 230 - ], - "flags": {}, - "order": 14, - "mode": 0, - "inputs": [ - { - "name": "model", - "type": "MODEL", - "link": 4, - "slot_index": 0 - }, - { - "name": "clip", - "type": "CLIP", - "link": 5 - }, - { - "name": "vae", - "type": "VAE", - "link": 6 - }, - { - "name": "image", - "type": "IMAGE", - "link": 24 - }, - { - "name": "negative", - "type": "STRING", - "link": 15, - "widget": { - "name": "negative", - "config": [ - "STRING", - { - "multiline": true, - "placeholder": "negative", - "default": "" - } - ] - }, - "slot_index": 4 - }, - { - "name": "denoise", - "type": "FLOAT", - "link": 28, - "widget": { - "name": "denoise", - "config": [ - "FLOAT", - { - "default": 0.6, - "min": 0, - "max": 1, - "step": 0.01 - } - ] - }, - "slot_index": 5 - } - ], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 25 - ], - "shape": 3, - "slot_index": 0 - }, - { - "name": "LATENT", - "type": "LATENT", - "links": null, - "shape": 3 - } - ], - "title": "## i2i", - "properties": { - "Node name for S&R": "## i2i [cac944]" - }, - "widgets_values": [ - "A blond girl in a pink dress, clear sky, 1958", - "", - 0.2599999999999997 - ] - }, - { - "id": 15, - "type": "SaveImage", - "pos": [ - 1054, - 67 - ], - "size": [ - 1110.1168110198978, - 630.0240418701173 - ], - "flags": {}, - "order": 15, - "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 25 - } - ], - "properties": {}, - "widgets_values": [ - "ComfyUI" - ] - }, - { - "id": 17, - "type": "Animation Builder (mtb)", - "pos": [ - -1032, - 1274 - ], - "size": [ - 210, - 306 - ], - "flags": {}, - "order": 2, - "mode": 0, - "outputs": [ - { - "name": "frame", - "type": "INT", - "links": null, - "shape": 3 - }, - { - "name": "0-1 (scaled)", - "type": "FLOAT", - "links": [ - 27, - 29 - ], - "shape": 3, - "slot_index": 1 - }, - { - "name": "count", - "type": "INT", - "links": null, - "shape": 3 - }, - { - "name": "loop_ended", - "type": "BOOL", - "links": [ - 16 - ], - "shape": 3, - "slot_index": 3 - } - ], - "properties": { - "Node name for S&R": "Animation Builder (mtb)" - }, - "widgets_values": [ - 30, - 1, - 1, - 90, - 1, - "raw: 90\nframe: 0", - "Done 😎!", - "reset", - "queue" - ] - }, - { - "id": 18, - "type": "Get Batch From History (mtb)", - "pos": [ - -369, - 1511 - ], - "size": [ - 235.1999969482422, - 118 - ], - "flags": {}, - "order": 8, - "mode": 0, - "inputs": [ - { - "name": "passthrough_image", - "type": "IMAGE", - "link": null - }, - { - "name": "enable", - "type": "BOOL", - "link": 16, - "widget": { - "name": "enable", - "config": [ - "BOOL", - { - "default": true - } - ] - }, - "slot_index": 1 - } - ], - "outputs": [ - { - "name": "i", - "type": "IMAGE", - "links": [ - 17, - 18 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "Get Batch From History (mtb)" - }, - "widgets_values": [ - false, - 30, - 0 - ] - } - ], - "links": [ - [ - 4, - 9, - 0, - 8, - 0, - "MODEL" - ], - [ - 5, - 9, - 1, - 8, - 1, - "CLIP" - ], - [ - 6, - 9, - 2, - 8, - 2, - "VAE" - ], - [ - 8, - 13, - 0, - 12, - 0, - "INT" - ], - [ - 9, - 12, - 0, - 11, - 1, - "BOOL" - ], - [ - 10, - 10, - 0, - 11, - 0, - "IMAGE" - ], - [ - 15, - 16, - 1, - 8, - 4, - "STRING" - ], - [ - 16, - 17, - 3, - 18, - 1, - "BOOL" - ], - [ - 17, - 18, - 0, - 20, - 0, - "IMAGE" - ], - [ - 18, - 18, - 0, - 19, - 0, - "IMAGE" - ], - [ - 24, - 14, - 0, - 8, - 3, - "IMAGE" - ], - [ - 25, - 8, - 0, - 15, - 0, - "IMAGE" - ], - [ - 26, - 11, - 0, - 14, - 0, - "IMAGE" - ], - [ - 27, - 17, - 1, - 22, - 0, - "FLOAT" - ], - [ - 28, - 22, - 0, - 8, - 5, - "FLOAT" - ], - [ - 29, - 17, - 1, - 23, - 0, - "FLOAT" - ], - [ - 30, - 23, - 0, - 14, - 1, - "FLOAT" - ] - ], - "groups": [ - { - "title": "Init Image", - "bounding": [ - -1730, - 215, - 716, - 713 - ], - "color": "#3f789e", - "locked": false - }, - { - "title": "Video Output", - "bounding": [ - -426, - 1176, - 1223, - 787 - ], - "color": "#3f789e", - "locked": false - }, - { - "title": "Animation", - "bounding": [ - -473, - 363, - 900, - 718 - ], - "color": "#3f789e", - "locked": false - } - ], - "config": {}, - "extra": {}, - "version": 0.4 -} \ No newline at end of file