diff --git a/README.en.md b/README.en.md index db69a41..ee0cbae 100644 --- a/README.en.md +++ b/README.en.md @@ -33,6 +33,13 @@ **v1.1.2 (2024/3/25)** +PS: Please update [ComfyUI_IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) to v2, and moved v1 models to **ComfyUI\models\ipadapter** (Otherwise, the latest model is automatically downloaded from Huggingface) +
+- Added `easy ipadapterApply` +- Added `easy ipadapterApplyADV` + +(4c25580) + - `easy kSamplerInpainting` add *additional* widget,you can choose 'Differential Diffusion' or 'Only InpaintModelConditioning' - Fixed `easy pipeEdit` error when add lora to prompt - Fixed layerDiffuse xyplot bug diff --git a/README.md b/README.md index 62fde05..ab191e1 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ - 简化 Stable Cascade [示例参考](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableCascade) - 简化 Layer Diffuse [示例参考](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#LayerDiffusion), 首次使用您可能需要运行 `pip install -r requirements.txt` 安装所需依赖 - 简化 InstantID [示例参考](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#InstantID), 需先保证自定义节点包中安装了 [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID) +- 简化 IPAdapter, 需先保证自定义节点包中安装最新版v2的 [ComfyUI_IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) - 扩展 XYplot 的可用性 - 整合了Fooocus Inpaint功能 - 整合了常用的逻辑计算、转换类型、展示所有类型等 @@ -36,11 +37,18 @@ **v1.1.2 (2024/3/25)** +PS: 请更新至最新版v2的 [ComfyUI_IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus), 并移动v1版本模型文件至 ComfyUI\models\ipadapter (否则会自动从huggingface下载最新模型) +
+- 增加 `easy ipadapterApply` +- 增加 `easy ipadapterApplyADV` + +(4c25580) + - `easy kSamplerInpainting` 增加 *additional* 属性,可设置成 Differential Diffusion 或 Only InpaintModelConditioning - 修复 `easy pipeEdit` 提示词输入lora时报错 - 修复 layerDiffuse xyplot相关bug -**v1.1.1 (2024/3/21)** +**v1.1.1 (5c8af8f)** - 修复首次添加含seed的节点且当前模式为control_before_generate时,seed为0的问题 - `easy preSamplingAdvanced` 增加 **return_with_leftover_noise** diff --git a/py/config.py b/py/config.py index 1733335..cbe0958 100644 --- a/py/config.py +++ b/py/config.py @@ -141,4 +141,95 @@ REMBG_MODELS = { "RMBG-1.4": { "model_url": "https://huggingface.co/briaai/RMBG-1.4/resolve/main/model.pth" } +} + +#ipadapter +IPADAPTER_DIR = os.path.join(folder_paths.models_dir, "ipadapter") +IPADAPTER_MODELS = { + "LIGHT - SD1.5 only (low strength)": { + "sd15": { + "model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15_light_v11.bin" + }, + "sdxl": { + "model_url": "" + } + }, + "STANDARD (medium strength)": { + "sd15": { + "model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15.safetensors" + }, + "sdxl": { + "model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter_sdxl.safetensors" + } + }, + "VIT-G (medium strength)": { + "sd15": { + "model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15_vit-G.safetensors" + }, + "sdxl": { + "model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter_sdxl_vit-h.safetensors" + } + }, + "PLUS (high strength)": { + "sd15": { + "model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter-plus_sd15.safetensors" + }, + "sdxl": { + "model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter-plus_sdxl_vit-h.safetensors" + } + }, + "PLUS FACE (portraits)": { + "sd15": { + "model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter-plus-face_sd15.safetensors" + }, + "sdxl": { + "model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter-plus-face_sdxl_vit-h.safetensors" + } + }, + "FULL FACE - SD1.5 only (portraits stronger)": { + "sd15": { + "model_url": "https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter-full-face_sd15.safetensors" + }, + "sdxl": { + "model_url": "" + } + }, + "FACEID": { + "sd15": { + "model_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid_sd15.bin", + "lora_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid_sd15_lora.safetensors" + }, + "sdxl": { + "model_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid_sdxl.bin", + "lora_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid_sdxl_lora.safetensors" + } + }, + "FACEID PLUS - SD1.5 only": { + "sd15": { + "model_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plus_sd15.bin", + "lora_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plus_sd15_lora.safetensors" + }, + "sdxl": { + "model_url": "", + "lora_url": "" + } + }, + "FACEID PLUS V2": { + "sd15": { + "model_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plusv2_sd15.bin", + "lora_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plusv2_sd15_lora.safetensors" + }, + "sdxl": { + "model_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plusv2_sdxl.bin", + "lora_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plusv2_sdxl_lora.safetensors" + } + }, + "FACEID PORTRAIT (style transfer)": { + "sd15": { + "model_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-portrait-v11_sd15.bin", + }, + "sdxl": { + "model_url": "https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-portrait_sdxl.bin", + } + } } \ No newline at end of file diff --git a/py/easyNodes.py b/py/easyNodes.py index b938db8..9ae62f3 100644 --- a/py/easyNodes.py +++ b/py/easyNodes.py @@ -6,18 +6,19 @@ from comfy.sd import CLIP, VAE from comfy.model_patcher import ModelPatcher from comfy_extras.chainner_models import model_loading from comfy_extras.nodes_mask import LatentCompositeMasked +from comfy.clip_vision import load as load_clip_vision from urllib.request import urlopen from PIL import Image from server import PromptServer from nodes import MAX_RESOLUTION, LatentFromBatch, RepeatLatentBatch, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS, ConditioningSetMask, ConditioningConcat, CLIPTextEncode, VAEEncodeForInpaint, InpaintModelConditioning -from .config import MAX_SEED_NUM, BASE_RESOLUTIONS, RESOURCES_DIR, INPAINT_DIR, FOOOCUS_STYLES_DIR, FOOOCUS_INPAINT_HEAD, FOOOCUS_INPAINT_PATCH +from .config import MAX_SEED_NUM, BASE_RESOLUTIONS, RESOURCES_DIR, INPAINT_DIR, FOOOCUS_STYLES_DIR, FOOOCUS_INPAINT_HEAD, FOOOCUS_INPAINT_PATCH, IPADAPTER_DIR, IPADAPTER_MODELS from .log import log_node_info, log_node_error, log_node_warn from .wildcards import process_with_loras, get_wildcard_list, process from .adv_encode import advanced_encode from .layer_diffuse.func import LayerDiffuse, LayerMethod -from .libs.utils import find_wildcards_seed, is_linked_styles_selector, easySave, get_local_filepath, add_folder_path_and_extensions, get_sd_version +from .libs.utils import find_wildcards_seed, is_linked_styles_selector, easySave, get_local_filepath, add_folder_path_and_extensions from .libs.loader import easyLoader from .libs.sampler import easySampler from .libs.xyplot import easyXYPlot @@ -40,6 +41,7 @@ add_folder_path_and_extensions("onnx", [os.path.join(model_path, "onnx")], {'.on add_folder_path_and_extensions("instantid", [os.path.join(model_path, "instantid")], folder_paths.supported_pt_extensions) add_folder_path_and_extensions("layer_model", [os.path.join(model_path, "layer_model")], folder_paths.supported_pt_extensions) add_folder_path_and_extensions("rembg", [os.path.join(model_path, "rembg")], folder_paths.supported_pt_extensions) +add_folder_path_and_extensions("ipadapter", [os.path.join(model_path, "ipadapter")], folder_paths.supported_pt_extensions) # ---------------------------------------------------------------提示词 开始----------------------------------------------------------------------# @@ -1543,9 +1545,9 @@ class LLLiteLoader: return (model_lllite,) -#---------------------------------------------------------------测试 开始----------------------------------------------------------------------# +#---------------------------------------------------------------Inpaint 开始----------------------------------------------------------------------# -# FooocusInpaint (Testing) +# FooocusInpaint from .fooocus import InpaintHead, InpaintWorker inpaint_head_model = None class fooocusInpaintLoader: @@ -1560,7 +1562,7 @@ class fooocusInpaintLoader: RETURN_TYPES = ("INPAINT_PATCH",) RETURN_NAMES = ("patch",) - CATEGORY = "EasyUse/__for_testing" + CATEGORY = "EasyUse/Inpaint" FUNCTION = "apply" def apply(self, head, patch): @@ -1577,6 +1579,345 @@ class fooocusInpaintLoader: return ((inpaint_head_model, inpaint_lora),) +#---------------------------------------------------------------适配器 开始----------------------------------------------------------------------# +def insightface_loader(provider): + try: + from insightface.app import FaceAnalysis + except ImportError as e: + raise Exception(e) + + path = os.path.join(folder_paths.models_dir, "insightface") + model = FaceAnalysis(name="buffalo_l", root=path, providers=[provider + 'ExecutionProvider', ]) + model.prepare(ctx_id=0, det_size=(640, 640)) + return model + +# Apply Ipadapter +class ipadapter: + + def __init__(self): + self.normol_presets = [ + 'LIGHT - SD1.5 only (low strength)', + 'STANDARD (medium strength)', + 'VIT-G (medium strength)', + 'PLUS (high strength)', + 'PLUS FACE (portraits)', + 'FULL FACE - SD1.5 only (portraits stronger)' + ] + self.faceid_presets = [ + 'FACEID', + 'FACEID PLUS - SD1.5 only', + 'FACEID PLUS V2', + 'FACEID PORTRAIT (style transfer)' + ] + self.presets = self.normol_presets + self.faceid_presets + + + def error(self): + raise Exception(f"[ERROR] To use ipadapterApply, you need to install 'ComfyUI_IPAdapter_plus'") + + def get_clipvision_file(self, preset, node_name): + preset = preset.lower() + clipvision_list = folder_paths.get_filename_list("clip_vision") + + if preset.startswith("vit-g"): + pattern = '(ViT.bigG.14.*39B.b160k|ipadapter.*sdxl|sdxl.*model\.(bin|safetensors))' + else: + pattern = '(ViT.H.14.*s32B.b79K|ipadapter.*sd15|sd1.?5.*model\.(bin|safetensors))' + clipvision_files = [e for e in clipvision_list if re.search(pattern, e, re.IGNORECASE)] + + clipvision_name = clipvision_files[0] if len(clipvision_files)>0 else None + clipvision_file = folder_paths.get_full_path("clip_vision", clipvision_name) if clipvision_name else None + if clipvision_name is not None: + log_node_info(node_name, f"Using {clipvision_name}") + + return clipvision_file, clipvision_name + + def get_ipadapter_file(self, preset, is_sdxl, node_name): + preset = preset.lower() + ipadapter_list = folder_paths.get_filename_list("ipadapter") + is_insightface = False + lora_pattern = None + + if preset.startswith("light"): + if is_sdxl: + raise Exception("light model is not supported for SDXL") + pattern = 'sd15.light.v11\.(safetensors|bin)$' + # if light model v11 is not found, try with the old version + if not [e for e in ipadapter_list if re.search(pattern, e, re.IGNORECASE)]: + pattern = 'sd15.light\.(safetensors|bin)$' + elif preset.startswith("standard"): + if is_sdxl: + pattern = 'ip.adapter.sdxl.vit.h\.(safetensors|bin)$' + else: + pattern = 'ip.adapter.sd15\.(safetensors|bin)$' + elif preset.startswith("vit-g"): + if is_sdxl: + pattern = 'ip.adapter.sdxl\.(safetensors|bin)$' + else: + pattern = 'sd15.vit.g\.(safetensors|bin)$' + elif preset.startswith("plus ("): + if is_sdxl: + pattern = 'plus.sdxl.vit.h\.(safetensors|bin)$' + else: + pattern = 'ip.adapter.plus.sd15\.(safetensors|bin)$' + elif preset.startswith("plus face"): + if is_sdxl: + pattern = 'plus.face.sdxl.vit.h\.(safetensors|bin)$' + else: + pattern = 'plus.face.sd15\.(safetensors|bin)$' + elif preset.startswith("full"): + if is_sdxl: + raise Exception("full face model is not supported for SDXL") + pattern = 'full.face.sd15\.(safetensors|bin)$' + elif preset.startswith("faceid portrait"): + if is_sdxl: + raise Exception("portrait model is not supported for SDXL") + pattern = 'portrait.sd15\.(safetensors|bin)$' + is_insightface = True + elif preset == "faceid": + if is_sdxl: + pattern = 'faceid.sdxl\.(safetensors|bin)$' + lora_pattern = 'faceid.sdxl.lora\.safetensors$' + else: + pattern = 'faceid.sd15\.(safetensors|bin)$' + lora_pattern = 'faceid.sd15.lora\.safetensors$' + is_insightface = True + elif preset.startswith("faceid plus -"): + if is_sdxl: + raise Exception("faceid plus model is not supported for SDXL") + pattern = 'faceid.plus.sd15\.(safetensors|bin)$' + lora_pattern = 'faceid.plus.sd15.lora\.safetensors$' + is_insightface = True + elif preset.startswith("faceid plus v2"): + if is_sdxl: + pattern = 'faceid.plusv2.sdxl\.(safetensors|bin)$' + lora_pattern = 'faceid.plusv2.sdxl.lora\.safetensors$' + else: + pattern = 'faceid.plusv2.sd15\.(safetensors|bin)$' + lora_pattern = 'faceid.plusv2.sd15.lora\.safetensors$' + is_insightface = True + else: + raise Exception(f"invalid type '{preset}'") + + ipadapter_files = [e for e in ipadapter_list if re.search(pattern, e, re.IGNORECASE)] + ipadapter_name = ipadapter_files[0] if len(ipadapter_files)>0 else None + ipadapter_file = folder_paths.get_full_path("ipadapter", ipadapter_name) if ipadapter_name else None + if ipadapter_name is not None: + log_node_info(node_name, f"Using {ipadapter_name}") + + return ipadapter_file, ipadapter_name, is_insightface, lora_pattern + + def get_lora_file(self, preset, pattern, model_type, model, model_strength, clip_strength, clip=None): + lora_list = folder_paths.get_filename_list("loras") + lora_files = [e for e in lora_list if re.search(pattern, e, re.IGNORECASE)] + lora_name = lora_files[0] if lora_files else None + if lora_name: + return easyCache.load_lora({"model": model, "clip": clip, "lora_name": lora_name, "model_strength":model_strength, "clip_strength":clip_strength},) + else: + if "lora_url" in IPADAPTER_MODELS[preset][model_type]: + lora_name = get_local_filepath(IPADAPTER_MODELS[preset][model_type]["lora_url"], os.path.join(folder_paths.models_dir, "loras")) + return easyCache.load_lora({"model": model, "clip": clip, "lora_name": lora_name, "model_strength":model_strength, "clip_strength":clip_strength},) + return (model, clip) + + def ipadapter_model_loader(self, file): + model = comfy.utils.load_torch_file(file, safe_load=True) + + if file.lower().endswith(".safetensors"): + st_model = {"image_proj": {}, "ip_adapter": {}} + for key in model.keys(): + if key.startswith("image_proj."): + st_model["image_proj"][key.replace("image_proj.", "")] = model[key] + elif key.startswith("ip_adapter."): + st_model["ip_adapter"][key.replace("ip_adapter.", "")] = model[key] + model = st_model + del st_model + + if not "ip_adapter" in model.keys() or not model["ip_adapter"]: + raise Exception("invalid IPAdapter model {}".format(file)) + + if 'plusv2' in file.lower(): + model["faceidplusv2"] = True + + return model + + def load_model(self, model, preset, lora_model_strength, provider="CPU", clip_vision=None, optional_ipadapter=None, cache_mode='none', node_name='easy ipadapterApply'): + pipeline = {"clipvision": {'file': None, 'model': None}, "ipadapter": {'file': None, 'model': None}, + "insightface": {'provider': None, 'model': None}} + if optional_ipadapter is not None: + pipeline = optional_ipadapter + + # 1. Load the clipvision model + if not clip_vision: + clipvision_file, clipvision_name = self.get_clipvision_file(preset, node_name) + if clipvision_file is None: + raise Exception("ClipVision model not found.") + if clipvision_file == pipeline['clipvision']['file']: + clip_vision = pipeline['clipvision']['model'] + elif cache_mode in ["all", "clip_vision only"] and clipvision_name in cache: + log_node_info("easy ipadapterApply", f"Using ClipModel {clipvision_name} Cached") + clip_vision = cache[clipvision_name][1] + else: + clip_vision = load_clip_vision(clipvision_file) + update_cache(clipvision_name, (False, clip_vision)) + pipeline['clipvision']['file'] = clipvision_file + pipeline['clipvision']['model'] = clip_vision + + # 2. Load the ipadapter model + is_sdxl = isinstance(model.model, comfy.model_base.SDXL) + ipadapter_file, ipadapter_name, is_insightface, lora_pattern = self.get_ipadapter_file(preset, is_sdxl, node_name) + model_type = 'sdxl' if is_sdxl else 'sd15' + if ipadapter_file is None: + ipadapter_file = get_local_filepath(IPADAPTER_MODELS[preset][model_type]["model_url"], IPADAPTER_DIR) + ipadapter = self.ipadapter_model_loader(ipadapter_file) + pipeline['ipadapter']['file'] = ipadapter_file + pipeline['ipadapter']['model'] = ipadapter + + # 3. Load the lora model if needed + if lora_pattern is not None: + if lora_model_strength > 0: + model, _ = self.get_lora_file(preset, lora_pattern, model_type, model, lora_model_strength, 1) + + # 4. Load the insightface model if needed + if is_insightface: + icache_key = 'insightface-' + provider + if provider == pipeline['insightface']['provider']: + insightface = pipeline['insightface']['model'] + elif icache_key in cache: + log_node_info("easy ipadapterApply", f"Using InsightFaceModel {icache_key} Cached") + insightface = cache[icache_key][1] + else: + insightface = insightface_loader(provider) + update_cache(icache_key, (False, insightface)) + pipeline['insightface']['provider'] = provider + pipeline['insightface']['model'] = insightface + + return (model, pipeline,) + +class ipadapterApply(ipadapter): + def __init__(self): + super().__init__() + pass + + @classmethod + def INPUT_TYPES(cls): + presets = cls().presets + return { + "required": { + "model": ("MODEL",), + "image": ("IMAGE",), + "preset": (presets,), + "lora_strength": ("FLOAT", {"default": 0.6, "min": 0, "max": 1, "step": 0.01}), + "provider": (["CPU", "CUDA", "ROCM", "DirectML", "OpenVINO", "CoreML"],), + "weight": ("FLOAT", {"default": 1.0, "min": -1, "max": 3, "step": 0.05}), + "weight_faceidv2": ("FLOAT", { "default": 1.0, "min": -1, "max": 5.0, "step": 0.05 }), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "cache_mode": (["insightface only", "clip_vision only", "all", "none"], {"default": "insightface only"},), + "use_tiled": ("BOOLEAN", {"default": False},), + }, + + "optional": { + "attn_mask": ("MASK",), + "optional_ipadapter": ("IPADAPTER",), + } + } + + RETURN_TYPES = ("MODEL", "IMAGE", "MASK", "IPADAPTER",) + RETURN_NAMES = ("model", "tiles", "masks", "ipadapter", ) + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def apply(self, model, image, preset, lora_strength, provider, weight, weight_faceidv2, start_at, end_at, cache_mode, use_tiled, attn_mask=None, optional_ipadapter=None): + tiles, masks = [None], [None] + model, ipadapter = self.load_model(model, preset, lora_strength, provider, clip_vision=None, optional_ipadapter=optional_ipadapter, cache_mode=cache_mode) + if use_tiled: + if "IPAdapterTiled" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterTiled"] + model, tiles, masks = cls().apply_tiled(model, ipadapter, image, weight, "linear", start_at, end_at, sharpening=0.0, combine_embeds="concat", image_negative=None, attn_mask=attn_mask, clip_vision=None, embeds_scaling='V only') + else: + if preset in ['FACEID PLUS V2', 'FACEID PORTRAIT (style transfer)']: + if "IPAdapterAdvanced" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterAdvanced"] + model, = cls().apply_ipadapter(model, ipadapter, image, weight, "linear", start_at, end_at, combine_embeds="concat", weight_faceidv2=weight_faceidv2, image_negative=None, clip_vision=None, attn_mask=attn_mask, insightface=None, embeds_scaling='V only') + else: + if "IPAdapter" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapter"] + model, = cls().apply_ipadapter(model, ipadapter, image, weight, start_at, end_at, attn_mask) + + return (model, tiles, masks, ipadapter) + +class ipadapterApplyAdvanced(ipadapter): + def __init__(self): + super().__init__() + pass + + @classmethod + def INPUT_TYPES(cls): + presets = cls().presets + WEIGHT_TYPES = ["linear", "ease in", "ease out", 'ease in-out', 'reverse in-out', 'weak input', 'weak output', + 'weak middle', 'strong middle', 'style transfer (SDXL)'] + return { + "required": { + "model": ("MODEL",), + "image": ("IMAGE",), + "preset": (presets,), + "lora_strength": ("FLOAT", {"default": 0.6, "min": 0, "max": 1, "step": 0.01}), + "provider": (["CPU", "CUDA", "ROCM", "DirectML", "OpenVINO", "CoreML"],), + "weight": ("FLOAT", {"default": 1.0, "min": -1, "max": 3, "step": 0.05}), + "weight_faceidv2": ("FLOAT", {"default": 1.0, "min": -1, "max": 5.0, "step": 0.05 }), + "weight_type": (WEIGHT_TYPES,), + "combine_embeds": (["concat", "add", "subtract", "average", "norm average"],), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "embeds_scaling": (['V only', 'K+V', 'K+V w/ C penalty', 'K+mean(V) w/ C penalty'],), + "cache_mode": (["insightface only", "clip_vision only", "all", "none"], {"default": "insightface only"},), + "use_tiled": ("BOOLEAN", {"default": False},), + "use_batch": ("BOOLEAN", {"default": False},), + "sharpening": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05}), + }, + + "optional": { + "image_negative": ("IMAGE",), + "attn_mask": ("MASK",), + "clip_vision": ("CLIP_VISION",), + "optional_ipadapter": ("IPADAPTER",), + } + } + + RETURN_TYPES = ("MODEL", "IMAGE", "MASK", "IPADAPTER",) + RETURN_NAMES = ("model", "tiles", "masks", "ipadapter", ) + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def apply(self, model, image, preset, lora_strength, provider, weight, weight_faceidv2, weight_type, combine_embeds, start_at, end_at, embeds_scaling, cache_mode, use_tiled, use_batch, sharpening, image_negative=None, clip_vision=None, attn_mask=None, optional_ipadapter=None): + tiles, masks = [None], [None] + model, ipadapter = self.load_model(model, preset, lora_strength, provider, clip_vision=clip_vision, optional_ipadapter=optional_ipadapter, cache_mode=cache_mode) + if use_tiled: + if use_batch: + if "IPAdapterTiledBatch" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterTiledBatch"] + else: + if "IPAdapterTiled" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterTiled"] + model, tiles, masks = cls().apply_tiled(model, ipadapter, image, weight, weight_type, start_at, end_at, sharpening=sharpening, combine_embeds=combine_embeds, image_negative=image_negative, attn_mask=attn_mask, clip_vision=clip_vision, embeds_scaling=embeds_scaling) + else: + if use_batch: + if "IPAdapterBatch" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterBatch"] + else: + if "IPAdapterAdvanced" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterAdvanced"] + model, = cls().apply_ipadapter(model, ipadapter, image, weight, weight_type, start_at, end_at, combine_embeds=combine_embeds, weight_faceidv2=weight_faceidv2, image_negative=image_negative, clip_vision=clip_vision, attn_mask=attn_mask, insightface=None, embeds_scaling=embeds_scaling) + + return (model, tiles, masks, ipadapter) + #Apply InstantID class instantID: @@ -1670,7 +2011,7 @@ class instantIDApply(instantID): OUTPUT_NODE = True FUNCTION = "apply" - CATEGORY = "EasyUse/__for_testing" + CATEGORY = "EasyUse/Adapter" def apply(self, pipe, image, instantid_file, insightface, control_net_name, cn_strength, cn_soft_weights, weight, start_at, end_at, noise, image_kps=None, mask=None, control_net=None, prompt=None, extra_pnginfo=None, my_unique_id=None): @@ -1718,7 +2059,7 @@ class instantIDApplyAdvanced(instantID): OUTPUT_NODE = True FUNCTION = "apply_advanced" - CATEGORY = "EasyUse/__for_testing" + CATEGORY = "EasyUse/Adapter" def apply_advanced(self, pipe, image, instantid_file, insightface, control_net_name, cn_strength, cn_soft_weights, weight, start_at, end_at, noise, image_kps=None, mask=None, control_net=None, positive=None, negative=None, prompt=None, extra_pnginfo=None, my_unique_id=None): @@ -5326,6 +5667,9 @@ class showLoaderSettingsNames: NODE_CLASS_MAPPINGS = { + # seed 随机种 + "easy seed": easySeed, + "easy globalSeed": globalSeed, # prompt 提示词 "easy positive": positivePrompt, "easy negative": negativePrompt, @@ -5344,12 +5688,16 @@ NODE_CLASS_MAPPINGS = { "easy controlnetLoader": controlnetSimple, "easy controlnetLoaderADV": controlnetAdvanced, "easy LLLiteLoader": LLLiteLoader, + # Adapter 适配器 + "easy ipadapterApply": ipadapterApply, + "easy ipadapterApplyADV": ipadapterApplyAdvanced, + "easy instantIDApply": instantIDApply, + "easy instantIDApplyADV": instantIDApplyAdvanced, + # Inpaint 内补 + "easy fooocusInpaintLoader": fooocusInpaintLoader, # latent 潜空间 "easy latentNoisy": latentNoisy, "easy latentCompositeMaskedWithCond": latentCompositeMaskedWithCond, - # seed 随机种 - "easy seed": easySeed, - "easy globalSeed": globalSeed, # preSampling 预采样处理 "easy preSampling": samplerSettings, "easy preSamplingAdvanced": samplerSettingsAdvanced, @@ -5404,13 +5752,12 @@ NODE_CLASS_MAPPINGS = { "easy showLoaderSettingsNames": showLoaderSettingsNames, # "easy imageRemoveBG": imageREMBG, "dynamicThresholdingFull": dynamicThresholdingFull, - # __for_testing 测试 - "easy fooocusInpaintLoader": fooocusInpaintLoader, - "easy instantIDApply": instantIDApply, - "easy instantIDApplyADV": instantIDApplyAdvanced, } NODE_DISPLAY_NAME_MAPPINGS = { + # seed 随机种 + "easy seed": "EasySeed", + "easy globalSeed": "EasyGlobalSeed", # prompt 提示词 "easy positive": "Positive", "easy negative": "Negative", @@ -5429,12 +5776,16 @@ NODE_DISPLAY_NAME_MAPPINGS = { "easy controlnetLoader": "EasyControlnet", "easy controlnetLoaderADV": "EasyControlnet (Advanced)", "easy LLLiteLoader": "EasyLLLite", + # Adapter 适配器 + "easy ipadapterApply": "Easy Apply IPAdapter", + "easy ipadapterApplyADV": "Easy Apply IPAdapter (Advanced)", + "easy instantIDApply": "Easy Apply InstantID", + "easy instantIDApplyADV": "Easy Apply InstantID (Advanced)", + # Inpaint 内补 + "easy fooocusInpaintLoader": "Load Fooocus Inpaint", # latent 潜空间 "easy latentNoisy": "LatentNoisy", "easy latentCompositeMaskedWithCond": "LatentCompositeMaskedWithCond", - # seed 随机种 - "easy seed": "EasySeed", - "easy globalSeed": "EasyGlobalSeed", # preSampling 预采样处理 "easy preSampling": "PreSampling", "easy preSamplingAdvanced": "PreSampling (Advanced)", @@ -5489,8 +5840,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { "easy showLoaderSettingsNames": "Show Loader Settings Names", "easy imageRemoveBG": "ImageRemoveBG", "dynamicThresholdingFull": "DynamicThresholdingFull", - # __for_testing 测试 - "easy fooocusInpaintLoader": "Load Fooocus Inpaint", - "easy instantIDApply": "Easy Apply InstantID", - "easy instantIDApplyADV": "Easy Apply InstantID (Advanced)", } \ No newline at end of file diff --git a/py/image.py b/py/image.py index 845f34c..89a5366 100644 --- a/py/image.py +++ b/py/image.py @@ -1,5 +1,4 @@ from PIL import Image -from enum import Enum import os import hashlib import folder_paths @@ -7,37 +6,7 @@ import torch import numpy as np from nodes import MAX_RESOLUTION from .log import log_node_info - - -def pil2tensor(image): - return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) - -class ResizeMode(Enum): - RESIZE = "Just Resize" - INNER_FIT = "Crop and Resize" - OUTER_FIT = "Resize and Fill" - def int_value(self): - if self == ResizeMode.RESIZE: - return 0 - elif self == ResizeMode.INNER_FIT: - return 1 - elif self == ResizeMode.OUTER_FIT: - return 2 - assert False, "NOTREACHED" - -RESIZE_MODES = [ResizeMode.RESIZE.value, ResizeMode.INNER_FIT.value, ResizeMode.OUTER_FIT.value] - -def get_new_bounds(width, height, left, right, top, bottom): - """Returns the new bounds for an image with inset crop data.""" - left = 0 + left - right = width - right - top = 0 + top - bottom = height - bottom - return (left, right, top, bottom) - -# Tensor to PIL -def tensor2pil(image): - return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) +from .libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds # 图像裁切 class imageInsetCrop: @@ -324,6 +293,7 @@ class imageScaleDownToSize(imageScaleDownBy): class imagePixelPerfect: @classmethod def INPUT_TYPES(s): + RESIZE_MODES = [ResizeMode.RESIZE.value, ResizeMode.INNER_FIT.value, ResizeMode.OUTER_FIT.value] return { "required": { "image": ("IMAGE",), diff --git a/py/libs/image.py b/py/libs/image.py new file mode 100644 index 0000000..31ae691 --- /dev/null +++ b/py/libs/image.py @@ -0,0 +1,35 @@ +import torch +import numpy as np +from enum import Enum +from PIL import Image + +# PIL to Tensor +def pil2tensor(image): + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) +# Tensor to PIL +def tensor2pil(image): + return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) + +# Get new bounds +def get_new_bounds(width, height, left, right, top, bottom): + """Returns the new bounds for an image with inset crop data.""" + left = 0 + left + right = width - right + top = 0 + top + bottom = height - bottom + return (left, right, top, bottom) + + +class ResizeMode(Enum): + RESIZE = "Just Resize" + INNER_FIT = "Crop and Resize" + OUTER_FIT = "Resize and Fill" + def int_value(self): + if self == ResizeMode.RESIZE: + return 0 + elif self == ResizeMode.INNER_FIT: + return 1 + elif self == ResizeMode.OUTER_FIT: + return 2 + assert False, "NOTREACHED" + diff --git a/py/libs/utils.py b/py/libs/utils.py index 86bc141..28a759b 100644 --- a/py/libs/utils.py +++ b/py/libs/utils.py @@ -154,31 +154,4 @@ def easySave(images, filename_prefix, output_type, prompt=None, extra_pnginfo=No return results['ui']['images'] else: results = SaveImage().save_images(images, filename_prefix, prompt, extra_pnginfo) - return results['ui']['images'] - -# Image Utils -# from PIL import Image, ImageDraw -# import numpy as np -# import torch -# def is_image_transparent(img): -# print(img.shape) -# if len(img.shape) > 3 and img.shape[3] == 4: -# return True -# else: -# m = tensor2pil(img) -# if m.mode == "RGBA": -# return True -# else: -# return False -# -# def create_grid(image_size, box_size): -# img = Image.new('RGBA', image_size, (255, 255, 255, 255)) # 白色背景 -# draw = ImageDraw.Draw(img) -# -# for x in range(0, img.width, box_size): -# for y in range(0, img.height, box_size): -# if (x // box_size % 2 == 0 and y // box_size % 2 == 0) or (x // box_size % 2 == 1 and y // box_size % 2 == 1): -# draw.rectangle([(x, y), (x+box_size, y+box_size)], fill=(204, 204, 204, 255)) # 不透明 -# else: -# continue # 保持透明 -# return img + return results['ui']['images'] \ No newline at end of file diff --git a/web/js/easy/easyDynamicWidgets.js b/web/js/easy/easyDynamicWidgets.js index c231866..af88754 100644 --- a/web/js/easy/easyDynamicWidgets.js +++ b/web/js/easy/easyDynamicWidgets.js @@ -4,13 +4,13 @@ import { ComfyWidgets } from "/scripts/widgets.js"; let origProps = {}; +const seedNodes = ["easy seed", "easy latentNoisy", "easy wildcards", "easy preSampling", "easy preSamplingAdvanced", "easy preSamplingNoiseIn", "easy preSamplingSdTurbo", "easy preSamplingCascade", "easy preSamplingDynamicCFG", "easy preSamplingLayerDiffusion", "easy fullkSampler", "easy fullCascadeKSampler"] +const loaderNodes = ["easy fullLoader", "easy a1111Loader", "easy comfyLoader"] const findWidgetByName = (node, name) => node.widgets.find((w) => w.name === name); const doesInputWithNameExist = (node, name) => node.inputs ? node.inputs.some((input) => input.name === name) : false; -function updateNodeHeight(node) { - node.setSize([node.size[0], node.computeSize()[1]]); -} +function updateNodeHeight(node) {node.setSize([node.size[0], node.computeSize()[1]]);} function toggleWidget(node, widget, show = false, suffix = "") { if (!widget || doesInputWithNameExist(node, widget.name)) return; @@ -26,7 +26,6 @@ function toggleWidget(node, widget, show = false, suffix = "") { const height = show ? Math.max(node.computeSize()[1], origSize[1]) : node.size[1]; node.setSize([node.size[0], height]); - } function widgetLogic(node, widget) { @@ -208,6 +207,45 @@ function widgetLogic(node, widget) { toggleWidget(node, findWidgetByName(node, 'new_cond_end'), true) } } + + if (widget.name === 'preset') { + const normol_presets = [ + 'LIGHT - SD1.5 only (low strength)', + 'STANDARD (medium strength)', + 'VIT-G (medium strength)', + 'PLUS (high strength)', 'PLUS FACE (portraits)', + 'FULL FACE - SD1.5 only (portraits stronger)', + 'FACEID PORTRAIT (style transfer)' + ] + const faceid_presets = [ + 'FACEID', + 'FACEID PLUS - SD1.5 only', + 'FACEID PLUS V2', + ] + if(normol_presets.includes(widget.value)){ + toggleWidget(node, findWidgetByName(node, 'lora_strength')) + toggleWidget(node, findWidgetByName(node, 'provider')) + toggleWidget(node, findWidgetByName(node, 'weight_faceidv2')) + } + else if(faceid_presets.includes(widget.value)){ + if(widget.value == 'FACEID PLUS V2'){ + toggleWidget(node, findWidgetByName(node, 'weight_faceidv2'), true) + }else{ + toggleWidget(node, findWidgetByName(node, 'weight_faceidv2')) + } + toggleWidget(node, findWidgetByName(node, 'lora_strength'), true) + toggleWidget(node, findWidgetByName(node, 'provider'), true) + } + updateNodeHeight(node) + } + + if (widget.name === 'use_tiled') { + if(widget.value) + toggleWidget(node, findWidgetByName(node, 'sharpening'), true) + else + toggleWidget(node, findWidgetByName(node, 'sharpening')) + updateNodeHeight(node) + } } function widgetLogic2(node, widget) { @@ -486,6 +524,8 @@ app.registerExtension({ case "easy rangeFloat": case 'easy latentCompositeMaskedWithCond': case 'easy pipeEdit': + case 'easy ipadapterApply': + case 'easy ipadapterApplyADV': getSetters(node) break case "easy wildcards": @@ -734,7 +774,7 @@ app.registerExtension({ }; } - if (["easy fullLoader", "easy a1111Loader", "easy comfyLoader"].includes(nodeData.name)) { + if (loaderNodes.includes(nodeData.name)) { function populate(text, type = 'positive') { if (this.widgets) { const pos = this.widgets.findIndex((w) => w.name === type + "_prompt"); @@ -781,7 +821,7 @@ app.registerExtension({ }; } - if (["easy seed", "easy latentNoisy", "easy wildcards", "easy preSampling", "easy preSamplingAdvanced", "easy preSamplingNoiseIn", "easy preSamplingSdTurbo", "easy preSamplingCascade", "easy preSamplingDynamicCFG", "easy preSamplingLayerDiffusion", "easy fullkSampler", "easy fullCascadeKSampler"].includes(nodeData.name)) { + if (seedNodes.includes(nodeData.name)) { const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = async function () { onNodeCreated ? onNodeCreated.apply(this, []) : undefined; @@ -922,7 +962,7 @@ const getSetWidgets = ['rescale_after_model', 'rescale', 'refiner_lora1_name', 'refiner_lora2_name', 'upscale_method', 'image_output', 'add_noise', 'info', 'sampler_name', 'ckpt_B_name', 'ckpt_C_name', 'save_model', 'refiner_ckpt_name', - 'num_loras', 'mode', 'toggle', 'resolution', 'target_parameter', 'input_count', 'replace_count', 'downscale_mode', 'range_mode','text_combine_mode', 'input_mode','lora_count','ckpt_count', 'conditioning_mode'] + 'num_loras', 'mode', 'toggle', 'resolution', 'target_parameter', 'input_count', 'replace_count', 'downscale_mode', 'range_mode','text_combine_mode', 'input_mode','lora_count','ckpt_count', 'conditioning_mode', 'preset', 'use_tiled', 'use_batch'] function getSetters(node) { if (node.widgets) diff --git a/web/js/easy/easyExtraMenu.js b/web/js/easy/easyExtraMenu.js index 2ad5ac7..c1d4a33 100644 --- a/web/js/easy/easyExtraMenu.js +++ b/web/js/easy/easyExtraMenu.js @@ -4,6 +4,7 @@ const loaders = ['easy fullLoader', 'easy a1111Loader', 'easy comfyLoader'] const preSampling = ['easy preSampling', 'easy preSamplingAdvanced', 'easy preSamplingDynamicCFG', 'easy preSamplingNoiseIn', 'easy preSamplingLayerDiffusion', 'easy fullkSampler'] const kSampler = ['easy kSampler', 'easy kSamplerTiled', 'easy kSamplerInpainting', 'easy kSamplerDownscaleUnet', 'easy kSamplerLayerDiffusion'] const controlnet = ['easy controlnetLoader', 'easy controlnetLoaderADV', 'easy instantIDApply', 'easy instantIDApplyADV'] +const ipadapter = ['easy ipadapterApply', 'easy ipadapterApplyADV'] const positive_prompt = ['easy positive', 'easy wildcards'] const widgetMapping = { "positive_prompt":{ @@ -45,6 +46,17 @@ const widgetMapping = { "cn_strength": ["strength", "cn_strength"], "cn_soft_weights": ["scale_soft_weights","cn_soft_weights"], }, + "ipadapter":{ + "preset":"preset", + "lora_strength": "lora_strength", + "provider": "provider", + "weight":"weight", + "weight_faceidv2": "weight_faceidv2", + "start_at": "start_at", + "end_at": "end_at", + "cache_mode": "cache_mode", + "use_tiled": "use_tiled", + } } const inputMapping = { "loaders":{ @@ -73,6 +85,12 @@ const inputMapping = { "positive_prompt":{ }, + "ipadapter":{ + "model":"model", + "image":"image", + "attn_mask":"attn_mask", + "optional_ipadapter":"optional_ipadapter" + } }; const outputMapping = { @@ -104,6 +122,12 @@ const outputMapping = { "load_image":{ "IMAGE":"IMAGE", "MASK": "MASK" + }, + "ipadapter":{ + "model":"model", + "tiles":"tiles", + "masks":"masks", + "ipadapter":"ipadapter" } }; @@ -491,6 +515,10 @@ app.registerExtension({ if (controlnet.includes(nodeData.name)) { addMenu("↪️ Swap EasyControlnet", 'controlnet', controlnet, nodeType) } + // Swap IPAdapater + if (ipadapter.includes(nodeData.name)) { + addMenu("↪️ Swap EasyIPAdapater", 'ipadapter', ipadapter, nodeType) + } } });