From 90d3451cd970d5aa9cac55224e24a7c7fd98d253 Mon Sep 17 00:00:00 2001 From: matt3o Date: Fri, 2 Feb 2024 20:06:27 +0100 Subject: [PATCH] add experimental tiled IPAdapter --- IPAdapterPlus.py | 221 ++++++++++-- README.md | 31 +- examples/IPAdapter_tiled.json | 649 ++++++++++++++++++++++++++++++++++ 3 files changed, 856 insertions(+), 45 deletions(-) create mode 100644 examples/IPAdapter_tiled.json diff --git a/IPAdapterPlus.py b/IPAdapterPlus.py index 1322289..425a931 100644 --- a/IPAdapterPlus.py +++ b/IPAdapterPlus.py @@ -1,15 +1,15 @@ import torch -import contextlib import os import math import comfy.utils import comfy.model_management -from comfy.clip_vision import clip_preprocess +from comfy.clip_vision import clip_preprocess, Output from comfy.ldm.modules.attention import optimized_attention +from nodes import MAX_RESOLUTION import folder_paths -from torch import nn +import torch.nn as nn from PIL import Image import torch.nn.functional as F import torchvision.transforms as TT @@ -167,6 +167,82 @@ def set_model_patch_replace(model, patch_kwargs, key): else: to["patches_replace"]["attn2"][key].set_new_condition(**patch_kwargs) +def masked_tiling(image, short_side_tiles, weight=0.6, blur=0): + _, orig_height, orig_width, _ = image.shape + tile_size = 224 + + if orig_width < orig_height: + num_tiles_x = short_side_tiles + new_width = tile_size * num_tiles_x + new_height = orig_height * new_width // orig_width + num_tiles_y = new_height // tile_size + else: + num_tiles_y = short_side_tiles + new_height = tile_size * num_tiles_y + new_width = orig_width * new_height // orig_height + num_tiles_x = new_width // tile_size + + start_x = start_y = 0 + if (new_height % tile_size) >= tile_size//4: + num_tiles_y += 1 + else: + start_y = (new_height - tile_size*num_tiles_y) // 2 + + if (new_width % tile_size) >= tile_size//4: + num_tiles_x += 1 + else: + start_x = (new_width - tile_size*num_tiles_x) // 2 + + weight = 1.0 if num_tiles_x == 1 or num_tiles_y == 1 else weight + + ref_image = F.interpolate(image.permute([0,3,1,2]), size=(new_height, new_width), mode="bicubic").permute([0,2,3,1]) + + tiles = [] + attn_mask = [] + + for i in range(num_tiles_y): + for j in range(num_tiles_x): + start_height = i * tile_size + start_y + end_height = start_height + tile_size + start_y + start_width = j * tile_size + start_x + end_width = start_width + tile_size + start_x + + if end_height > new_height: + start_height = new_height - tile_size + end_height = new_height + if end_width > new_width: + start_width = new_width - tile_size + end_width = new_width + + tile = ref_image[:1, start_height:end_height, start_width:end_width, :] + tiles.append(tile.squeeze(0)) + + # create mask + mask = torch.zeros([new_height, new_width], dtype=image.dtype, device=image.device) + mask[start_height:end_height, start_width:end_width] = weight + + attn_mask.append(mask) + + image = torch.stack(tiles, dim=0) + attn_mask = torch.stack(attn_mask, dim=0) + + # If we have a lot of tiles we add bigger tiles with a higher weight to give clipvision a better idea of the overall image + if num_tiles_x != 1 and num_tiles_y != 1: + comp_tiles, comp_attn_mask = masked_tiling(ref_image, 1, 1.0, blur) + + if image.shape[1:3] != comp_tiles.shape[1:3]: + comp_tiles = F.interpolate(comp_tiles.permute([0,3,1,2]), size=(image.shape[1], image.shape[2]), mode="bicubic").permute([0,2,3,1]) + image = torch.cat([comp_tiles, image], dim=0) + + if attn_mask.shape[1:3] != comp_attn_mask.shape[1:3]: + comp_attn_mask = F.interpolate(comp_attn_mask.unsqueeze(1), size=(attn_mask.shape[1], attn_mask.shape[2]), mode="bicubic").squeeze(1) + attn_mask = torch.cat([comp_attn_mask, attn_mask], dim=0) + + if blur > 0: + attn_mask = TT.GaussianBlur(int(6*blur+1), blur)(attn_mask.unsqueeze(1)).permute([0,2,3,1]).squeeze(-1) + + return image, attn_mask + def image_add_noise(image, noise): image = image.permute([0,3,1,2]) torch.manual_seed(0) # use a fixed random for reproducible results @@ -187,8 +263,23 @@ def zeroed_hidden_states(clip_vision, batch_size): comfy.model_management.load_model_gpu(clip_vision.patcher) pixel_values = clip_preprocess(image.to(clip_vision.load_device)).float() outputs = clip_vision.model(pixel_values=pixel_values, intermediate_output=-2) + # we only need the penultimate hidden states - outputs = outputs[1].to(comfy.model_management.intermediate_device()) + return outputs[1].to(comfy.model_management.intermediate_device()) + +def encode_image_masked(clip_vision, image, mask=None): + comfy.model_management.load_model_gpu(clip_vision.patcher) + pixel_values = clip_preprocess(image.to(clip_vision.load_device)).float() + + if mask is not None: + pixel_values = pixel_values * mask.to(clip_vision.load_device) + + out = clip_vision.model(pixel_values=pixel_values, intermediate_output=-2) + + outputs = Output() + outputs["last_hidden_state"] = out[0].to(comfy.model_management.intermediate_device()) + outputs["image_embeds"] = out[2].to(comfy.model_management.intermediate_device()) + outputs["penultimate_hidden_states"] = out[1].to(comfy.model_management.intermediate_device()) return outputs def min_(tensor_list): @@ -238,8 +329,8 @@ def contrast_adaptive_sharpening(image, amount): div = torch.reciprocal(1 + 4*w) output = ((b + d + f + h)*w + e) * div - output = output.clamp(0, 1) output = torch.nan_to_num(output) + output = output.clamp(0, 1) return (output) @@ -430,7 +521,6 @@ class CrossAttentionPatch: if weight_type.startswith("channel"): # code by Lvmin Zhang at Stanford University as also seen on Fooocus IPAdapter implementation - # please read licensing notes https://github.com/lllyasviel/Fooocus/blob/69a23c4d60c9e627409d0cb0f8862cdb015488eb/extras/ip_adapter.py#L234 ip_v_mean = torch.mean(ip_v, dim=1, keepdim=True) ip_v_offset = ip_v - ip_v_mean _, _, C = ip_k.shape @@ -510,7 +600,7 @@ class IPAdapterModelLoader: elif key.startswith("ip_adapter."): st_model["ip_adapter"][key.replace("ip_adapter.", "")] = model[key] model = st_model - + if not "ip_adapter" in model.keys() or not model["ip_adapter"]: raise Exception("invalid IPAdapter model {}".format(ckpt_path)) @@ -566,11 +656,32 @@ class IPAdapterApply: } } - RETURN_TYPES = ("MODEL", ) + RETURN_TYPES = ("MODEL",) FUNCTION = "apply_ipadapter" CATEGORY = "ipadapter" - def apply_ipadapter(self, ipadapter, model, weight, clip_vision=None, image=None, weight_type="original", noise=None, embeds=None, attn_mask=None, start_at=0.0, end_at=1.0, unfold_batch=False, insightface=None, faceid_v2=False, weight_v2=False): + def apply_ipadapter(self, + ipadapter, + model, + weight, + clip_vision=None, + image=None, + weight_type="original", + noise=None, + embeds=None, + attn_mask=None, + start_at=0.0, + end_at=1.0, + unfold_batch=False, + insightface=None, + faceid_v2=False, + weight_v2=False, + clip_vision_mask=None, + short_side_tiles=0, + tile_weight=0.0, + tile_blur=0, + ): + self.dtype = torch.float16 if comfy.model_management.should_use_fp16() else torch.float32 self.device = comfy.model_management.get_torch_device() self.weight = weight @@ -578,6 +689,10 @@ class IPAdapterApply: self.is_portrait = "proj.2.weight" in ipadapter["image_proj"] and not "proj.3.weight" in ipadapter["image_proj"] and not "0.to_q_lora.down.weight" in ipadapter["ip_adapter"] self.is_faceid = self.is_portrait or "0.to_q_lora.down.weight" in ipadapter["ip_adapter"] self.is_plus = (self.is_full or "latents" in ipadapter["image_proj"] or "perceiver_resampler.proj_in.weight" in ipadapter["image_proj"]) + is_tiled = True if short_side_tiles > 0 else False + + if is_tiled: + image, attn_mask = masked_tiling(image, short_side_tiles, tile_weight, tile_blur) if self.is_faceid and not insightface: raise Exception('InsightFace must be provided for FaceID models.') @@ -633,7 +748,7 @@ class IPAdapterApply: if image.shape[1] != image.shape[2]: print("\033[33mINFO: the IPAdapter reference image is not a square, CLIPImageProcessor will resize and crop it at the center. If the main focus of the picture is not in the middle the result might not be what you are expecting.\033[0m") - clip_embed = clip_vision.encode_image(image) + clip_embed = encode_image_masked(clip_vision, image, clip_vision_mask) neg_image = image_add_noise(image, noise) if noise > 0 else None if self.is_plus: @@ -674,52 +789,92 @@ class IPAdapterApply: image_prompt_embeds = image_prompt_embeds.to(self.device, dtype=self.dtype) uncond_image_prompt_embeds = uncond_image_prompt_embeds.to(self.device, dtype=self.dtype) - work_model = model.clone() + self.work_model = model.clone() if attn_mask is not None: attn_mask = attn_mask.to(self.device) - sigma_start = model.model.model_sampling.percent_to_sigma(start_at) - sigma_end = model.model.model_sampling.percent_to_sigma(end_at) + sigma_start = self.work_model.model.model_sampling.percent_to_sigma(start_at) + sigma_end = self.work_model.model.model_sampling.percent_to_sigma(end_at) + + if is_tiled: + for i in range(image_prompt_embeds.shape[0]): #TODO: check if we can apply one mask per image in the attention patch phase + patch_kwargs = { + "number": 0, + "weight": self.weight, + "ipadapter": self.ipadapter, + "cond": image_prompt_embeds[i].unsqueeze(0), + "uncond": uncond_image_prompt_embeds[i].unsqueeze(0), + "weight_type": weight_type, + "mask": attn_mask[i].unsqueeze(0), + "sigma_start": sigma_start, + "sigma_end": sigma_end, + "unfold_batch": unfold_batch, + } + self.apply_patch(patch_kwargs) + else: + patch_kwargs = { + "number": 0, + "weight": self.weight, + "ipadapter": self.ipadapter, + "cond": image_prompt_embeds, + "uncond": uncond_image_prompt_embeds, + "weight_type": weight_type, + "mask": attn_mask, + "sigma_start": sigma_start, + "sigma_end": sigma_end, + "unfold_batch": unfold_batch, + } + self.apply_patch(patch_kwargs) - patch_kwargs = { - "number": 0, - "weight": self.weight, - "ipadapter": self.ipadapter, - "cond": image_prompt_embeds, - "uncond": uncond_image_prompt_embeds, - "weight_type": weight_type, - "mask": attn_mask, - "sigma_start": sigma_start, - "sigma_end": sigma_end, - "unfold_batch": unfold_batch, - } + return (self.work_model, attn_mask, image,) + def apply_patch(self, patch_kwargs): if not self.is_sdxl: for id in [1,2,4,5,7,8]: # id of input_blocks that have cross attention - set_model_patch_replace(work_model, patch_kwargs, ("input", id)) + set_model_patch_replace(self.work_model, patch_kwargs, ("input", id)) patch_kwargs["number"] += 1 for id in [3,4,5,6,7,8,9,10,11]: # id of output_blocks that have cross attention - set_model_patch_replace(work_model, patch_kwargs, ("output", id)) + set_model_patch_replace(self.work_model, patch_kwargs, ("output", id)) patch_kwargs["number"] += 1 - set_model_patch_replace(work_model, patch_kwargs, ("middle", 0)) + set_model_patch_replace(self.work_model, patch_kwargs, ("middle", 0)) else: for id in [4,5,7,8]: # id of input_blocks that have cross attention block_indices = range(2) if id in [4, 5] else range(10) # transformer_depth for index in block_indices: - set_model_patch_replace(work_model, patch_kwargs, ("input", id, index)) + set_model_patch_replace(self.work_model, patch_kwargs, ("input", id, index)) patch_kwargs["number"] += 1 for id in range(6): # id of output_blocks that have cross attention block_indices = range(2) if id in [3, 4, 5] else range(10) # transformer_depth for index in block_indices: - set_model_patch_replace(work_model, patch_kwargs, ("output", id, index)) + set_model_patch_replace(self.work_model, patch_kwargs, ("output", id, index)) patch_kwargs["number"] += 1 for index in range(10): - set_model_patch_replace(work_model, patch_kwargs, ("middle", 0, index)) + set_model_patch_replace(self.work_model, patch_kwargs, ("middle", 0, index)) patch_kwargs["number"] += 1 - return (work_model, ) +class IPAdapterTilesMasked(IPAdapterApply): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "ipadapter": ("IPADAPTER", ), + "clip_vision": ("CLIP_VISION",), + "image": ("IMAGE",), + "model": ("MODEL", ), + "weight": ("FLOAT", { "default": 0.7, "min": -1, "max": 3, "step": 0.05 }), + "noise": ("FLOAT", { "default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01 }), + "weight_type": (["original", "linear", "channel penalty"], ), + "start_at": ("FLOAT", { "default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001 }), + "end_at": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001 }), + "short_side_tiles": ("INT", { "default": 1, "min": 0, "max": 12, "step": 1 }), + "tile_weight": ("FLOAT", { "default": 0.6, "min": 0.0, "max": 1.0, "step": 0.05 }), + #"tile_blur": ("INT", { "default": 0, "min": 0, "max": 112, "step": 1 }), + }, + } + RETURN_TYPES = ("MODEL",) + class IPAdapterApplyFaceID(IPAdapterApply): @classmethod def INPUT_TYPES(s): @@ -995,6 +1150,7 @@ class IPAdapterBatchEmbeds: NODE_CLASS_MAPPINGS = { "IPAdapterModelLoader": IPAdapterModelLoader, "IPAdapterApply": IPAdapterApply, + "IPAdapterTilesMasked": IPAdapterTilesMasked, "IPAdapterApplyFaceID": IPAdapterApplyFaceID, "IPAdapterApplyEncoded": IPAdapterApplyEncoded, "PrepImageForClipVision": PrepImageForClipVision, @@ -1009,6 +1165,7 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "IPAdapterModelLoader": "Load IPAdapter Model", "IPAdapterApply": "Apply IPAdapter", + "IPAdapterTilesMasked": "IPAdapter Masked Tiles (experimental)", "IPAdapterApplyFaceID": "Apply IPAdapter FaceID", "IPAdapterApplyEncoded": "Apply IPAdapter from Encoded", "PrepImageForClipVision": "Prepare Image For Clip Vision", diff --git a/README.md b/README.md index f63ca27..ede71eb 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ IPAdapter implementation that follows the ComfyUI way of doing things. The code ## Important updates +**2024/02/02**: Added experimental [tiled IPAdapter](#tiled-ipadapter). It lets you easily handle reference images that are not square. Can be useful for upscaling. + **2024/01/19**: Support for FaceID Portrait models. **2024/01/16**: Notably increased quality of FaceID Plus/v2 models. Check the [comparison](https://github.com/cubiq/ComfyUI_IPAdapter_plus/issues/195) of all face models. @@ -19,12 +21,6 @@ IPAdapter implementation that follows the ComfyUI way of doing things. The code **2023/11/29**: Added `unfold_batch` option to send the reference images sequentially to a latent batch. Useful for animations. -**2023/11/26**: Added [timestepping](#timestepping). You may need to delete the old nodes and recreate them. **Important:** For this to work you need to update ComfyUI to the latest version. - -**2023/11/24**: Support for multiple attention masks. - -**2023/11/23**: Small but important update: the new default location for the IPAdapter models is `ComfyUI/models/ipadapter`. **No panic**: the legacy `ComfyUI/custom_nodes/ComfyUI_IPAdapter_plus/models` location still works and nothing will break. - *(previous updates removed for better readability)* ## What is it? @@ -194,6 +190,22 @@ In the `Apply IPAdapter` node you can set a start and an end point. The IPAdapte timestepping +### Tiled IPAdapter + +This is an experimental node that automatically splits a reference image in quadrants. It can be especially useful when the reference image is not in 1:1 ratio as the Clip Vision encoder only works with 224x224 square images. + +The `short_side_tiles` parameter defines the number of tiles to use for ther shorter side of the reference image; the number of tiles for the other side are calculated automatically. If the image is in landscape or portrait mode that generally means that only 2 tiles are created. If the image is a square, the value must be at least `2` for the node to have any meaninful effect. + +**If the aspect ratio of the refence image and the latent are very diffirent the image will almost certainly be stretched or squished.** + +`tile_weight` is very important any time the `short_side_tiles` value is greater than 1. The default value of `0.6` should be good for most scenarios but you may need to lower it a little (0.5) for SDXL. **This parameter has no effect if the `short_side_tiles` is = 1.** + +The main IPAdapter `weight` should also be lowered, `0.7` is a good starting point. + +**Important:** With a high number of tiles we are going to reiterate on the same _concept_ multiple times so I suggest to lower the CFG or better use the CFG Rescale node. Please check the workflow in the examples directory for reference. + +The node is experimental and will likely change in the future. + ### FaceID FaceID is a new IPAdapter model that takes the embeddings from [InsightFace](https://github.com/deepinsight/insightface). As such you need to install `insightface` in your ComfyUI python environment. You may also need `onnxruntime` and `onnxruntime-gpu`. Note that your CUDA version might not be compatible with onnxruntime, in that case you can select the "CPU" provider from the `Load InsightFace model` node. @@ -229,10 +241,3 @@ If you are interested I've also implemented the same features for [Huggingface D - [laksjdjf](https://github.com/laksjdjf/IPAdapter-ComfyUI/) - [fooocus](https://github.com/lllyasviel/Fooocus/blob/main/fooocus_extras/ip_adapter.py) -## IPAdapter in the wild - -Let me know if you spot the IPAdapter in the wild or tag @latentvision in the video description! - -- For German speakers you can find interesting YouTube tutorials on [A Latent Place](https://www.youtube.com/watch?v=rAWn_0YOBU0). -- In Chinese [Introversify](https://www.youtube.com/watch?v=xl8f3oxZgY8) -- [Scott Detweiler](https://www.youtube.com/watch?v=xzGdynQDzsM) covered this extension. diff --git a/examples/IPAdapter_tiled.json b/examples/IPAdapter_tiled.json new file mode 100644 index 0000000..bc6a69a --- /dev/null +++ b/examples/IPAdapter_tiled.json @@ -0,0 +1,649 @@ +{ + "last_node_id": 27, + "last_link_id": 53, + "nodes": [ + { + "id": 1, + "type": "CheckpointLoaderSimple", + "pos": [ + 140, + 770 + ], + "size": { + "0": 300, + "1": 100 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 51 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 5, + 6 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "sd15/realisticVisionV51_v51VAE.safetensors" + ] + }, + { + "id": 2, + "type": "VAELoader", + "pos": [ + 890, + 820 + ], + "size": { + "0": 300, + "1": 60 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "VAE", + "type": "VAE", + "links": [ + 12 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAELoader" + }, + "widgets_values": [ + "vae-ft-mse-840000-ema-pruned.safetensors" + ] + }, + { + "id": 25, + "type": "IPAdapterTilesMasked", + "pos": [ + 530, + 170 + ], + "size": { + "0": 315, + "1": 262 + }, + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "ipadapter", + "type": "IPADAPTER", + "link": 48 + }, + { + "name": "clip_vision", + "type": "CLIP_VISION", + "link": 49 + }, + { + "name": "image", + "type": "IMAGE", + "link": 50 + }, + { + "name": "model", + "type": "MODEL", + "link": 51 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 52 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "IPAdapterTilesMasked" + }, + "widgets_values": [ + 0.65, + 0, + "original", + 0, + 1, + 1, + 0.6 + ] + }, + { + "id": 9, + "type": "KSampler", + "pos": [ + 880, + 500 + ], + "size": { + "0": 315, + "1": 262 + }, + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 31 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 8 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 9 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 10 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 11 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 11, + "fixed", + 30, + 6, + "ddpm", + "karras", + 1 + ] + }, + { + "id": 18, + "type": "RescaleCFG", + "pos": [ + 980, + 390 + ], + "size": [ + 210, + 58 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 52 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 31 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "RescaleCFG" + }, + "widgets_values": [ + 0.9 + ] + }, + { + "id": 4, + "type": "CLIPVisionLoader", + "pos": [ + 150, + 190 + ], + "size": { + "0": 300, + "1": 60 + }, + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "CLIP_VISION", + "type": "CLIP_VISION", + "links": [ + 49 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPVisionLoader" + }, + "widgets_values": [ + "IPAdapter_image_encoder_sd15.safetensors" + ] + }, + { + "id": 3, + "type": "IPAdapterModelLoader", + "pos": [ + 150, + 80 + ], + "size": { + "0": 300, + "1": 60 + }, + "flags": {}, + "order": 3, + "mode": 0, + "outputs": [ + { + "name": "IPADAPTER", + "type": "IPADAPTER", + "links": [ + 48 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "IPAdapterModelLoader" + }, + "widgets_values": [ + "ip-adapter-plus_sd15.safetensors" + ] + }, + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + 570, + 500 + ], + "size": { + "0": 210, + "1": 120 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 5 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 8 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 8, + "type": "CLIPTextEncode", + "pos": [ + 570, + 680 + ], + "size": { + "0": 210, + "1": 120 + }, + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 6 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 9 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 6, + "type": "LoadImage", + "pos": [ + 50, + 310 + ], + "size": { + "0": 404.8474426269531, + "1": 404.49200439453125 + }, + "flags": {}, + "order": 4, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 50 + ], + "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": 11, + "type": "VAEDecode", + "pos": [ + 1253, + 502 + ], + "size": { + "0": 140, + "1": 50 + }, + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 11 + }, + { + "name": "vae", + "type": "VAE", + "link": 12 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 53 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + } + }, + { + "id": 27, + "type": "SaveImage", + "pos": [ + 1450, + 503 + ], + "size": { + "0": 315, + "1": 58 + }, + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 53 + } + ], + "properties": {}, + "widgets_values": [ + "ComfyUI" + ] + }, + { + "id": 10, + "type": "EmptyLatentImage", + "pos": [ + 570, + 860 + ], + "size": { + "0": 210, + "1": 110 + }, + "flags": {}, + "order": 5, + "mode": 0, + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 10 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 512, + 512, + 1 + ] + } + ], + "links": [ + [ + 5, + 1, + 1, + 7, + 0, + "CLIP" + ], + [ + 6, + 1, + 1, + 8, + 0, + "CLIP" + ], + [ + 8, + 7, + 0, + 9, + 1, + "CONDITIONING" + ], + [ + 9, + 8, + 0, + 9, + 2, + "CONDITIONING" + ], + [ + 10, + 10, + 0, + 9, + 3, + "LATENT" + ], + [ + 11, + 9, + 0, + 11, + 0, + "LATENT" + ], + [ + 12, + 2, + 0, + 11, + 1, + "VAE" + ], + [ + 31, + 18, + 0, + 9, + 0, + "MODEL" + ], + [ + 48, + 3, + 0, + 25, + 0, + "IPADAPTER" + ], + [ + 49, + 4, + 0, + 25, + 1, + "CLIP_VISION" + ], + [ + 50, + 6, + 0, + 25, + 2, + "IMAGE" + ], + [ + 51, + 1, + 0, + 25, + 3, + "MODEL" + ], + [ + 52, + 25, + 0, + 18, + 0, + "MODEL" + ], + [ + 53, + 11, + 0, + 27, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file