From 65c54649a0cc855aaa5d7e07e33649c5a9fa4b7e Mon Sep 17 00:00:00 2001 From: yolain Date: Mon, 4 Mar 2024 00:53:29 +0800 Subject: [PATCH] Upgrade to v1.1.0 --- README.en.md | 11 +- README.md | 13 +- __init__.py | 2 +- py/config.py | 32 +- py/easyNodes.py | 242 ++++++++++++-- py/layer_diffusion.py | 322 +++++++++++++++++++ py/libs/utils.py | 19 ++ py/server.py | 2 +- py/wildcards.py | 2 +- web/js/easy/easy.js | 231 -------------- web/js/easy/easyDynamicWidgets.js | 6 +- web/js/easy/easyExtraMenu.js | 508 ++++++++++++++++++++++++++++++ web/js/easy/easyInterface.js | 1 - web/js/easy/easySaveImage.js | 4 +- 14 files changed, 1129 insertions(+), 266 deletions(-) create mode 100644 py/layer_diffusion.py create mode 100644 web/js/easy/easyExtraMenu.js diff --git a/README.en.md b/README.en.md index 360bdbf..4ed86e3 100644 --- a/README.en.md +++ b/README.en.md @@ -39,12 +39,17 @@ Usage:
## Changelog -**v1.0.9 [2024-3-2]** +**v1.1.0 (2024/3/4)** + +- Added `easy preSamplingLayerDiffusion` and `easy kSamplerLayerDiffusion` +- Added a convenient menu to right-click on nodes such as Loader, Presampler, Sampler, Controlnet, etc. to quickly replace nodes of the same type +- Added `easy instantIDApplyADV` can link positive and negative +- Fixed `easy instantIDApply` mask not input right +- +**v1.0.9 (ff1add1)** - Fixed the error when ComfyUI-Impack-Pack and ComfyUI_InstantID were not installed - Fixed `easy pipeIn` -- -(f9d01ff) - Added `easy instantIDApply` - you need installed [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID) fisrt, Workflow[Example](https://github.com/yolain/ComfyUI-Easy-Use/blob/main/README.en.md#InstantID) - Fixed `easy detailerFix` not added to the list of nodes available for saving images formatting extensions - Fixed `easy XYInputs: PromptSR` errors are reported when replacing negative prompts diff --git a/README.md b/README.md index f84d035..c416faa 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,18 @@ stage_c 与 stage_b 可以使用[checkpoints](https://huggingface.co/stabilityai ## 更新日志 -**v1.0.9 [2024-3-3]** +**v1.1.0 (2024/3/4)** + +- 增加 `easy preSamplingLayerDiffusion` 与 `easy kSamplerLayerDiffusion` (连接 `easy kSampler` 也能通) +- 增加 在 加载器、预采样、采样器、Controlnet等节点上右键可快速替换同类型节点的便捷菜单 +- 增加 `easy instantIDApplyADV` 可连入 positive 与 negative +- 修复 `easy instantIDApply` mask 未传入正确值 + +**v1.0.9 (ff1add1)** - 修复未安装 ComfyUI-Impack-Pack 和 ComfyUI_InstantID 时报错 - 修复 `easy pipeIn` - pipe设为可不必选 - -(f9d01ff) -- 新增 `easy instantIDApply` - 需要先安装 [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID), 工作流参考[示例](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#InstantID) +- 增加 `easy instantIDApply` - 需要先安装 [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID), 工作流参考[示例](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#InstantID) - 修复 `easy detailerFix` 未添加到保存图片格式化扩展名可用节点列表 - 修复 `easy XYInputs: PromptSR` 在替换负面提示词时报错 diff --git a/__init__.py b/__init__.py index 93c24fc..3af40a9 100644 --- a/__init__.py +++ b/__init__.py @@ -74,4 +74,4 @@ WEB_DIRECTORY = "./web" __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', "WEB_DIRECTORY"] -print('\033[34mComfy-Easy-Use (v1.0.9): \033[92mLoaded\033[0m') \ No newline at end of file +print('\033[34mComfy-Easy-Use (v1.1.0): \033[92mLoaded\033[0m') \ No newline at end of file diff --git a/py/config.py b/py/config.py index 4512320..40a21ab 100644 --- a/py/config.py +++ b/py/config.py @@ -38,7 +38,7 @@ INPAINT_DIR = os.path.join(folder_paths.models_dir, "inpaint") RESOURCES_DIR = os.path.join(Path(__file__).parent.parent, "resources") FOOOCUS_STYLES_DIR = os.path.join(Path(__file__).parent.parent, "styles") - +LAYER_DIFFUSION_DIR = os.path.join(folder_paths.models_dir, "layer_model") FOOOCUS_STYLES_SAMPLES = 'https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/' @@ -57,4 +57,34 @@ FOOOCUS_INPAINT_PATCH = { "inpaint (1.32GB)": { "model_url": "https://huggingface.co/lllyasviel/fooocus_inpaint/resolve/main/inpaint.fooocus.patch" }, +} + +LAYER_DIFFUSION_VAE = { + "encode": { + "model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/vae_transparent_encoder.safetensors" + }, + "decode": { + "model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/vae_transparent_decoder.safetensors" + } +} + +LAYER_DIFFUSION = { + "Only Transparent (Attention Injection)": { + "model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_xl_transparent_attn.safetensors" + }, + "Only Transparent (Conv Injection)": { + "model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_xl_transparent_conv.safetensors" + }, + "Foreground to Blending": { + "model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_xl_fg2ble.safetensors" + }, + "Foreground blending to Background": { + "model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_xl_fgble2bg.safetensors" + }, + "Background to Blending": { + "model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_xl_bg2ble.safetensors" + }, + "Background blending to Foreground": { + "model_url": "https://huggingface.co/LayerDiffusion/layerdiffusion-v1/resolve/main/layer_xl_bgble2fg.safetensors" + }, } \ No newline at end of file diff --git a/py/easyNodes.py b/py/easyNodes.py index d5690e8..444fff5 100644 --- a/py/easyNodes.py +++ b/py/easyNodes.py @@ -2,6 +2,7 @@ import sys, os, re, json, time, math import torch import folder_paths import comfy.utils, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management +from comfy.utils import load_torch_file from comfy.sd import CLIP, VAE from comfy.model_patcher import ModelPatcher from comfy_extras.chainner_models import model_loading @@ -11,12 +12,13 @@ from PIL import Image from server import PromptServer from nodes import MAX_RESOLUTION, RepeatLatentBatch, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS, ConditioningSetMask, ConditioningConcat, CLIPTextEncode -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, LAYER_DIFFUSION_DIR, LAYER_DIFFUSION_VAE, LAYER_DIFFUSION 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_diffusion import LayerMethod, TransparentVAEDecoder -from .libs.utils import find_nearest_steps, find_wildcards_seed, is_linked_styles_selector, easySave, get_local_filepath, add_folder_path_and_extensions +from .libs.utils import find_nearest_steps, find_wildcards_seed, is_linked_styles_selector, easySave, get_local_filepath, to_lora_patch_dict, add_folder_path_and_extensions from .libs.loader import easyLoader from .libs.sampler import easySampler from .libs.xyplot import easyXYPlot @@ -35,6 +37,7 @@ add_folder_path_and_extensions("mmdets", [os.path.join(model_path, "mmdets")], f add_folder_path_and_extensions("sams", [os.path.join(model_path, "sams")], folder_paths.supported_pt_extensions) add_folder_path_and_extensions("onnx", [os.path.join(model_path, "onnx")], {'.onnx'}) add_folder_path_and_extensions("instantid", [os.path.join(model_path, "instantid")], {'.bin'}) +add_folder_path_and_extensions("layer_model", [os.path.join(model_path, "layer_model")], {'.safetensors'}) # ---------------------------------------------------------------提示词 开始----------------------------------------------------------------------# @@ -1609,6 +1612,7 @@ class instantIDApply: "control_net": ("CONTROL_NET",), }, "hidden": { + "positive": None, "negative": None, "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID" }, } @@ -1623,11 +1627,11 @@ class instantIDApply: def error(self): raise Exception(f"[ERROR] To use instantIDApply, you need to install 'ComfyUI_InstantID'") - 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): + 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, positive=None, negative=None, prompt=None, extra_pnginfo=None, my_unique_id=None): instantid_model, insightface_model, face_embeds = None, None, None model = pipe['model'] - positive = pipe['positive'] - negative = pipe['negative'] + positive = positive if positive is not None else pipe['positive'] + negative = negative if negative is not None else pipe['negative'] # Load InstantID if "InstantIDModelLoader" in ALL_NODE_CLASS_MAPPINGS: load_instant_cls = ALL_NODE_CLASS_MAPPINGS["InstantIDModelLoader"] @@ -1644,7 +1648,7 @@ class instantIDApply: if "ApplyInstantID" in ALL_NODE_CLASS_MAPPINGS: instantid_apply = ALL_NODE_CLASS_MAPPINGS['ApplyInstantID'] control_net = easyControlnet().load_controlnet(control_net_name, control_net, cn_soft_weights) - model, positive, negative = instantid_apply().apply_instantid(instantid_model, insightface_model, control_net, image, model, positive, negative, start_at, end_at, weight=weight, ip_weight=None, cn_strength=cn_strength, noise=noise, image_kps=image_kps, mask=None) + model, positive, negative = instantid_apply().apply_instantid(instantid_model, insightface_model, control_net, image, model, positive, negative, start_at, end_at, weight=weight, ip_weight=None, cn_strength=cn_strength, noise=noise, image_kps=image_kps, mask=mask) else: self.error() @@ -1665,6 +1669,52 @@ class instantIDApply: del pipe return (new_pipe, model, positive, negative) + +#Apply InstantID Advanced + +class instantIDApplyAdvanced: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required":{ + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "instantid_file": (folder_paths.get_filename_list("instantid"),), + "insightface": (["CPU", "CUDA", "ROCM"],), + "control_net_name": (folder_paths.get_filename_list("controlnet"),), + "cn_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "cn_soft_weights": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001},), + "weight": ("FLOAT", {"default": .8, "min": 0.0, "max": 5.0, "step": 0.01, }), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, }), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001, }), + "noise": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.05, }), + }, + "optional": { + "image_kps": ("IMAGE",), + "mask": ("MASK",), + "control_net": ("CONTROL_NET",), + "positive": ("CONDITIONING",), + "negative": ("CONDITIONING",), + }, + "hidden": { + "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID" + }, + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "CONDITIONING", "CONDITIONING") + RETURN_NAMES = ("pipe", "model", "positive", "negative") + OUTPUT_NODE = True + + FUNCTION = "apply" + CATEGORY = "EasyUse/__for_testing" + + 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, positive=None, negative=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + + return instantIDApply().apply(pipe, image, instantid_file, insightface, control_net_name, cn_strength, cn_soft_weights, weight, start_at, end_at, noise, image_kps, mask, control_net, positive, negative, prompt, extra_pnginfo, my_unique_id) #---------------------------------------------------------------预采样 开始----------------------------------------------------------------------# # 预采样设置(基础) @@ -2019,6 +2069,70 @@ class cascadeSettings: return {"ui": {"value": [seed_num]}, "result": (new_pipe,)} +# layerDiffusion预采样参数 +class layerDiffusionSettings: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + { + "pipe": ("PIPE_LINE",), + "method": ([LayerMethod.FG_ONLY_ATTN.value, LayerMethod.FG_ONLY_CONV.value],), + "weight": ("FLOAT",{"default": 1.0, "min": -1, "max": 3, "step": 0.05},), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler_ancestral"}), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "simple"}), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "seed_num": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + }, + "optional": { + # "image_to_latent": ("IMAGE",), + # "latent": ("LATENT",), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + OUTPUT_NODE = True + + FUNCTION = "settings" + CATEGORY = "EasyUse/PreSampling" + + + def settings(self, pipe, method, weight, steps, cfg, sampler_name, scheduler, denoise, seed_num, prompt=None, extra_pnginfo=None, my_unique_id=None): + + new_pipe = { + "model": pipe['model'], + "positive": pipe['positive'], + "negative": pipe['negative'], + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": pipe['samples'], + "images": pipe['images'], + "seed": seed_num, + + "loader_settings": { + **pipe["loader_settings"], + "steps": steps, + "cfg": cfg, + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": denoise, + "add_noise": "enabled", + "layer_diffusion_method": method, + "layer_diffusion_weight": weight, + } + } + + del pipe + + return {"ui": {"value": [seed_num]}, "result": (new_pipe,)} # 预采样设置(动态CFG) from .dynthres_core import DynThresh @@ -2164,8 +2278,9 @@ class dynamicThresholdingFull: # 完整采样器 class samplerFull: - def __init__(self): - pass + def __init__(self) -> None: + self.vae_transparent_decoder = None + self.vae_transparent_encoder = None @classmethod def INPUT_TYPES(cls): @@ -2231,6 +2346,17 @@ class samplerFull: if add_noise == "disable": disable_noise = True + # LayerDiffusion + if "layer_diffusion_method" in pipe['loader_settings']: + method = LayerMethod(pipe['loader_settings']['layer_diffusion_method']) + weight = pipe['loader_settings']['layer_diffusion_weight'] if 'layer_diffusion_weight' in pipe['loader_settings'] else 1.0 + model_file = get_local_filepath(LAYER_DIFFUSION[method.value]["model_url"], LAYER_DIFFUSION_DIR) + layer_lora_state_dict = load_torch_file(model_file) + layer_lora_patch_dict = to_lora_patch_dict(layer_lora_state_dict) + work_model = samp_model.clone() + work_model.add_patches(layer_lora_patch_dict, weight) + samp_model = work_model + def downscale_model_unet(samp_model): if downscale_options is None: return samp_model @@ -2271,6 +2397,10 @@ class samplerFull: image_output, link_id, save_prefix, tile_size, prompt, extra_pnginfo, my_unique_id, preview_latent, force_full_denoise=force_full_denoise, disable_noise=disable_noise): + alpha = None + layer_diffusion_method = pipe['loader_settings']['layer_diffusion_method'] if 'layer_diffusion_method' in pipe['loader_settings'] else None + # LayerDiffusion Decode + # Downscale Model Unet if samp_model is not None: samp_model = downscale_model_unet(samp_model) @@ -2288,11 +2418,39 @@ class samplerFull: else: samp_images = samp_vae.decode(latent).cpu() + # LayerDiffusion Decode + if layer_diffusion_method is not None: + if self.vae_transparent_decoder is None: + decoder_file = get_local_filepath(LAYER_DIFFUSION_VAE['decode']["model_url"], LAYER_DIFFUSION_DIR) + self.vae_transparent_decoder = TransparentVAEDecoder( + load_torch_file(decoder_file), + device=comfy.model_management.get_torch_device(), + dtype=(torch.float16 if comfy.model_management.should_use_fp16() else torch.float32), + ) + + pixel = samp_images.movedim(-1, 1) # [B, H, W, C] => [B, C, H, W] + pixel_with_alpha = self.vae_transparent_decoder.decode_pixel(pixel, latent) + # [B, C, H, W] => [B, H, W, C] + pixel_with_alpha = pixel_with_alpha.movedim(1, -1) + image = pixel_with_alpha[..., 1:] + alpha = pixel_with_alpha[..., 0] + + # mask to image + out = image.movedim(-1, 1) + if out.shape[1] == 3: # RGB + out = torch.cat([out, torch.ones_like(out[:, :1, :, :])], dim=1) + for i in range(out.shape[0]): + out[i, 3, :, :] = alpha + + new_images = out.movedim(1, -1) + else: + new_images = samp_images + # 推理总耗时(包含解码) end_decode_time = int(time.time() * 1000) spent_time = '扩散:' + str((end_time-start_time)/1000)+'秒, 解码:' + str((end_decode_time-end_time)/1000)+'秒' - results = easySave(samp_images, save_prefix, image_output, prompt, extra_pnginfo) + results = easySave(new_images, save_prefix, image_output, prompt, extra_pnginfo) sampler.update_value_by_id("results", my_unique_id, results) # Clean loaded_objects @@ -2306,7 +2464,7 @@ class samplerFull: "clip": samp_clip, "samples": samp_samples, - "images": samp_images, + "images": new_images, "seed": samp_seed, "loader_settings": { @@ -2317,17 +2475,19 @@ class samplerFull: sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe) + result = (new_pipe, new_images, samp_images, alpha) if layer_diffusion_method is not None else sampler.get_output(new_pipe,) + del pipe if image_output in ("Hide", "Hide/Save"): return {"ui": {}, - "result": sampler.get_output(new_pipe, )} + "result": result} if image_output in ("Sender", "Sender/Save"): PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results}) return {"ui": {"images": results}, - "result": sampler.get_output(new_pipe, )} + "result": result} def process_xyPlot(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, steps, cfg, sampler_name, scheduler, denoise, @@ -2472,9 +2632,9 @@ class samplerSimple: def run(self, pipe, image_output, link_id, save_prefix, model=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): - return samplerFull.run(self, pipe, None, None,None,None,None, image_output, link_id, save_prefix, - None, model, None, None, None, None, None, None, - tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) + return samplerFull().run(pipe, None, None, None, None, None, image_output, link_id, save_prefix, + None, model, None, None, None, None, None, None, + None, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) # 简易采样器 (Tiled) class samplerSimpleTiled: @@ -2507,10 +2667,44 @@ class samplerSimpleTiled: CATEGORY = "EasyUse/Sampler" def run(self, pipe, tile_size=512, image_output='preview', link_id=0, save_prefix='ComfyUI', model=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): - return samplerFull.run(self, pipe, None, None,None,None,None, image_output, link_id, save_prefix, + return samplerFull().run(pipe, None, None,None,None,None, image_output, link_id, save_prefix, None, model, None, None, None, None, None, None, tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) +# 简易采样器 (LayerDiffusion) +class samplerSimpleLayerDiffusion: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "image_output": (["Hide", "Preview", "Save", "Hide/Save", "Sender", "Sender/Save"],{"default": "Preview"}), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}) + }, + "optional": { + "model": ("MODEL",), + }, + "hidden": { + "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", + "embeddingsList": (folder_paths.get_filename_list("embeddings"),) + } + } + + RETURN_TYPES = ("PIPE_LINE", "IMAGE", "IMAGE", "MASK") + RETURN_NAMES = ("pipe", "alpha_image", "original_image", "alpha") + OUTPUT_NODE = True + FUNCTION = "run" + CATEGORY = "EasyUse/Sampler" + + def run(self, pipe, image_output='preview', link_id=0, save_prefix='ComfyUI', model=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): + return samplerFull().run(pipe, None, None,None,None,None, image_output, link_id, save_prefix, + None, model, None, None, None, None, None, None, + None, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) + # 简易采样器(收缩Unet) class samplerSimpleDownscaleUnet: @@ -2574,7 +2768,7 @@ class samplerSimpleDownscaleUnet: "upscale_method": upscale_method } - return samplerFull.run(self, pipe, None, None,None,None,None, image_output, link_id, save_prefix, + return samplerFull().run(pipe, None, None,None,None,None, image_output, link_id, save_prefix, None, model, None, None, None, None, None, None, tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise, downscale_options) # 简易采样器 (内补) @@ -2670,7 +2864,8 @@ class samplerSimpleInpainting: else: new_pipe = pipe del pipe - return samplerFull.run(self, new_pipe, None, None,None,None,None, image_output, link_id, save_prefix, + + return samplerFull().run(new_pipe, None, None,None,None,None, image_output, link_id, save_prefix, None, model, None, None, None, None, None, None, tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) @@ -2996,7 +3191,7 @@ class samplerCascadeSimple: def run(self, pipe, image_output, link_id, save_prefix, model_c=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): - return samplerCascadeFull.run(self, pipe, None, None,None, None,None,None,None, image_output, link_id, save_prefix, + return samplerCascadeFull().run(pipe, None, None,None, None,None,None,None, image_output, link_id, save_prefix, None, None, None, model_c, tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) class unsampler: @@ -4730,10 +4925,12 @@ NODE_CLASS_MAPPINGS = { "easy preSamplingSdTurbo": sdTurboSettings, "easy preSamplingDynamicCFG": dynamicCFGSettings, "easy preSamplingCascade": cascadeSettings, + "easy preSamplingLayerDiffusion": layerDiffusionSettings, # kSampler k采样器 "easy fullkSampler": samplerFull, "easy kSampler": samplerSimple, "easy kSamplerTiled": samplerSimpleTiled, + "easy kSamplerLayerDiffusion": samplerSimpleLayerDiffusion, "easy kSamplerInpainting": samplerSimpleInpainting, "easy kSamplerDownscaleUnet": samplerSimpleDownscaleUnet, "easy kSamplerSDTurbo": samplerSDTurbo, @@ -4775,7 +4972,9 @@ NODE_CLASS_MAPPINGS = { # __for_testing 测试 "easy fooocusInpaintLoader": fooocusInpaintLoader, "easy instantIDApply": instantIDApply, + "easy instantIDApplyADV": instantIDApplyAdvanced, } + NODE_DISPLAY_NAME_MAPPINGS = { # prompt 提示词 "easy positive": "Positive", @@ -4807,10 +5006,12 @@ NODE_DISPLAY_NAME_MAPPINGS = { "easy preSamplingSdTurbo": "PreSampling (SDTurbo)", "easy preSamplingDynamicCFG": "PreSampling (DynamicCFG)", "easy preSamplingCascade": "PreSampling (Cascade)", + "easy preSamplingLayerDiffusion": "PreSampling (LayerDiffusion)", # kSampler k采样器 "easy kSampler": "EasyKSampler", "easy fullkSampler": "EasyKSampler (Full)", "easy kSamplerTiled": "EasyKSampler (Tiled Decode)", + "easy kSamplerLayerDiffusion": "EasyKSampler (LayerDiffusion)", "easy kSamplerInpainting": "EasyKSampler (Inpainting)", "easy kSamplerDownscaleUnet": "EasyKsampler (Downscale Unet)", "easy kSamplerSDTurbo": "EasyKSampler (SDTurbo)", @@ -4851,5 +5052,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "dynamicThresholdingFull": "DynamicThresholdingFull", # __for_testing 测试 "easy fooocusInpaintLoader": "Load Fooocus Inpaint", - "easy instantIDApply": "Easy Apply InstantID" + "easy instantIDApply": "Easy Apply InstantID", + "easy instantIDApplyADV": "Easy Apply InstantID (Advanced)", } \ No newline at end of file diff --git a/py/layer_diffusion.py b/py/layer_diffusion.py new file mode 100644 index 0000000..01f17fa --- /dev/null +++ b/py/layer_diffusion.py @@ -0,0 +1,322 @@ +import torch.nn as nn +import torch +import cv2 +import numpy as np + +from enum import Enum +from tqdm import tqdm +from typing import Optional, Tuple +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.models.modeling_utils import ModelMixin +from diffusers.models.unet_2d_blocks import UNetMidBlock2D, get_down_block, get_up_block + + +def zero_module(module): + """ + Zero out the parameters of a module and return it. + """ + for p in module.parameters(): + p.detach().zero_() + return module + + +class LayerMethod(Enum): + FG_ONLY_ATTN = "Only Transparent (Attention Injection)" + FG_ONLY_CONV = "Only Transparent (Conv Injection)" + FG_TO_BLEND = "Foreground to Blending" + FG_BLEND_TO_BG = "Foreground Blending to Background" + BG_TO_BLEND = "Background to Blending" + BG_BLEND_TO_FG = "Background Blending to Foreground" + +class LatentTransparencyOffsetEncoder(torch.nn.Module): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.blocks = torch.nn.Sequential( + torch.nn.Conv2d(4, 32, kernel_size=3, padding=1, stride=1), + nn.SiLU(), + torch.nn.Conv2d(32, 32, kernel_size=3, padding=1, stride=1), + nn.SiLU(), + torch.nn.Conv2d(32, 64, kernel_size=3, padding=1, stride=2), + nn.SiLU(), + torch.nn.Conv2d(64, 64, kernel_size=3, padding=1, stride=1), + nn.SiLU(), + torch.nn.Conv2d(64, 128, kernel_size=3, padding=1, stride=2), + nn.SiLU(), + torch.nn.Conv2d(128, 128, kernel_size=3, padding=1, stride=1), + nn.SiLU(), + torch.nn.Conv2d(128, 256, kernel_size=3, padding=1, stride=2), + nn.SiLU(), + torch.nn.Conv2d(256, 256, kernel_size=3, padding=1, stride=1), + nn.SiLU(), + zero_module(torch.nn.Conv2d(256, 4, kernel_size=3, padding=1, stride=1)), + ) + + def __call__(self, x): + return self.blocks(x) + + +# 1024 * 1024 * 3 -> 16 * 16 * 512 -> 1024 * 1024 * 3 +class UNet1024(ModelMixin, ConfigMixin): + @register_to_config + def __init__( + self, + in_channels: int = 3, + out_channels: int = 3, + down_block_types: Tuple[str] = ( + "DownBlock2D", + "DownBlock2D", + "DownBlock2D", + "DownBlock2D", + "AttnDownBlock2D", + "AttnDownBlock2D", + "AttnDownBlock2D", + ), + up_block_types: Tuple[str] = ( + "AttnUpBlock2D", + "AttnUpBlock2D", + "AttnUpBlock2D", + "UpBlock2D", + "UpBlock2D", + "UpBlock2D", + "UpBlock2D", + ), + block_out_channels: Tuple[int] = (32, 32, 64, 128, 256, 512, 512), + layers_per_block: int = 2, + mid_block_scale_factor: float = 1, + downsample_padding: int = 1, + downsample_type: str = "conv", + upsample_type: str = "conv", + dropout: float = 0.0, + act_fn: str = "silu", + attention_head_dim: Optional[int] = 8, + norm_num_groups: int = 4, + norm_eps: float = 1e-5, + ): + super().__init__() + + # input + self.conv_in = nn.Conv2d( + in_channels, block_out_channels[0], kernel_size=3, padding=(1, 1) + ) + self.latent_conv_in = zero_module( + nn.Conv2d(4, block_out_channels[2], kernel_size=1) + ) + + self.down_blocks = nn.ModuleList([]) + self.mid_block = None + self.up_blocks = nn.ModuleList([]) + + # down + output_channel = block_out_channels[0] + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=layers_per_block, + in_channels=input_channel, + out_channels=output_channel, + temb_channels=None, + add_downsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + attention_head_dim=( + attention_head_dim + if attention_head_dim is not None + else output_channel + ), + downsample_padding=downsample_padding, + resnet_time_scale_shift="default", + downsample_type=downsample_type, + dropout=dropout, + ) + self.down_blocks.append(down_block) + + # mid + self.mid_block = UNetMidBlock2D( + in_channels=block_out_channels[-1], + temb_channels=None, + dropout=dropout, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + resnet_time_scale_shift="default", + attention_head_dim=( + attention_head_dim + if attention_head_dim is not None + else block_out_channels[-1] + ), + resnet_groups=norm_num_groups, + attn_groups=None, + add_attention=True, + ) + + # up + reversed_block_out_channels = list(reversed(block_out_channels)) + output_channel = reversed_block_out_channels[0] + for i, up_block_type in enumerate(up_block_types): + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + input_channel = reversed_block_out_channels[ + min(i + 1, len(block_out_channels) - 1) + ] + + is_final_block = i == len(block_out_channels) - 1 + + up_block = get_up_block( + up_block_type, + num_layers=layers_per_block + 1, + in_channels=input_channel, + out_channels=output_channel, + prev_output_channel=prev_output_channel, + temb_channels=None, + add_upsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + attention_head_dim=( + attention_head_dim + if attention_head_dim is not None + else output_channel + ), + resnet_time_scale_shift="default", + upsample_type=upsample_type, + dropout=dropout, + ) + self.up_blocks.append(up_block) + prev_output_channel = output_channel + + # out + self.conv_norm_out = nn.GroupNorm( + num_channels=block_out_channels[0], num_groups=norm_num_groups, eps=norm_eps + ) + self.conv_act = nn.SiLU() + self.conv_out = nn.Conv2d( + block_out_channels[0], out_channels, kernel_size=3, padding=1 + ) + + def forward(self, x, latent): + sample_latent = self.latent_conv_in(latent) + sample = self.conv_in(x) + emb = None + + down_block_res_samples = (sample,) + for i, downsample_block in enumerate(self.down_blocks): + if i == 3: + sample = sample + sample_latent + + sample, res_samples = downsample_block(hidden_states=sample, temb=emb) + down_block_res_samples += res_samples + + sample = self.mid_block(sample, emb) + + for upsample_block in self.up_blocks: + res_samples = down_block_res_samples[-len(upsample_block.resnets) :] + down_block_res_samples = down_block_res_samples[ + : -len(upsample_block.resnets) + ] + sample = upsample_block(sample, res_samples, emb) + + sample = self.conv_norm_out(sample) + sample = self.conv_act(sample) + sample = self.conv_out(sample) + return sample + + +def checkerboard(shape): + return np.indices(shape).sum(axis=0) % 2 + + +def fill_checkerboard_bg(y: torch.Tensor) -> torch.Tensor: + alpha = y[..., :1] + fg = y[..., 1:] + B, H, W, C = fg.shape + cb = checkerboard(shape=(H // 64, W // 64)) + cb = cv2.resize(cb, (W, H), interpolation=cv2.INTER_NEAREST) + cb = (0.5 + (cb - 0.5) * 0.1)[None, ..., None] + cb = torch.from_numpy(cb).to(fg) + vis = fg * alpha + cb * (1 - alpha) + return vis + + +class TransparentVAEDecoder: + def __init__(self, sd, device, dtype): + self.load_device = device + self.dtype = dtype + + model = UNet1024(in_channels=3, out_channels=4) + model.load_state_dict(sd, strict=True) + model.to(self.load_device, dtype=self.dtype) + model.eval() + self.model = model + + @torch.no_grad() + def estimate_single_pass(self, pixel, latent): + y = self.model(pixel, latent) + return y + + @torch.no_grad() + def estimate_augmented(self, pixel, latent): + args = [ + [False, 0], + [False, 1], + [False, 2], + [False, 3], + [True, 0], + [True, 1], + [True, 2], + [True, 3], + ] + + result = [] + + for flip, rok in tqdm(args): + feed_pixel = pixel.clone() + feed_latent = latent.clone() + + if flip: + feed_pixel = torch.flip(feed_pixel, dims=(3,)) + feed_latent = torch.flip(feed_latent, dims=(3,)) + + feed_pixel = torch.rot90(feed_pixel, k=rok, dims=(2, 3)) + feed_latent = torch.rot90(feed_latent, k=rok, dims=(2, 3)) + + eps = self.estimate_single_pass(feed_pixel, feed_latent).clip(0, 1) + eps = torch.rot90(eps, k=-rok, dims=(2, 3)) + + if flip: + eps = torch.flip(eps, dims=(3,)) + + result += [eps] + + result = torch.stack(result, dim=0) + median = torch.median(result, dim=0).values + return median + + @torch.no_grad() + def decode_pixel(self, pixel, latent): + # pixel.shape = [B, C=3, H, W] + assert pixel.shape[1] == 3 + pixel = pixel.to(device=self.load_device, dtype=self.dtype) + latent = latent.to(device=self.load_device, dtype=self.dtype) + # y.shape = [B, C=4, H, W] + y = self.estimate_augmented(pixel, latent) + y = y.clip(0, 1) + assert y.shape[1] == 4 + return y + +class TransparentVAEEncoder: + def __init__(self, sd, device, dtype): + self.load_device = device + self.dtype = dtype + + model = LatentTransparencyOffsetEncoder() + model.load_state_dict(sd, strict=True) + model.to(device=self.load_device, dtype=self.dtype) + model.eval() + + self.model = model + diff --git a/py/libs/utils.py b/py/libs/utils.py index fc407d5..406aede 100644 --- a/py/libs/utils.py +++ b/py/libs/utils.py @@ -107,6 +107,25 @@ def get_local_filepath(url, dirname, local_file_name=None): download_url_to_file(url, destination) return destination +def to_lora_patch_dict(state_dict: dict) -> dict: + """ Convert raw lora state_dict to patch_dict that can be applied on + modelpatcher.""" + patch_dict = {} + for k, w in state_dict.items(): + model_key, patch_type, weight_index = k.split('::') + if model_key not in patch_dict: + patch_dict[model_key] = {} + if patch_type not in patch_dict[model_key]: + patch_dict[model_key][patch_type] = [None] * 16 + patch_dict[model_key][patch_type][int(weight_index)] = w + + patch_flat = {} + for model_key, v in patch_dict.items(): + for patch_type, weight_list in v.items(): + patch_flat[model_key] = (patch_type, weight_list) + + return patch_flat + def easySave(images, filename_prefix, output_type, prompt=None, extra_pnginfo=None): """Save or Preview Image""" from nodes import PreviewImage, SaveImage diff --git a/py/server.py b/py/server.py index 9d4fb38..fae89f2 100644 --- a/py/server.py +++ b/py/server.py @@ -127,7 +127,7 @@ def prompt_seed_update(json_data): if 'class_type' not in v: continue cls = v['class_type'] - if cls in ["easy wildcards","easy preSampling","easy preSamplingAdvanced","easy preSamplingSdTurbo","easy preSamplingDynamicCFG","easy preSamplingCascade","easy fullCascadeKSampler","easy fullkSampler","easy seed","easy latentNoisy"]: + if cls in ["easy wildcards","easy preSampling","easy preSamplingAdvanced","easy preSamplingSdTurbo","easy preSamplingDynamicCFG","easy preSamplingLayerDiffusion","easy preSamplingCascade","easy fullCascadeKSampler","easy fullkSampler","easy seed","easy latentNoisy"]: extra_data = next((x for x in workflow["nodes"] if str(x["id"]) == k), None) if extra_data is not None: inputs = extra_data.get('inputs') diff --git a/py/wildcards.py b/py/wildcards.py index 9cd29f6..003e6ee 100644 --- a/py/wildcards.py +++ b/py/wildcards.py @@ -282,7 +282,7 @@ def process_with_loras(wildcard_opt, model, clip, title="Positive", seed=None, c has_loras = True if loras != [] else False show_wildcard_prompt = True if has_noodle_key or has_loras else False - if can_load_lora: + if can_load_lora and has_loras: for lora_name, model_weight, clip_weight, lbw, lbw_a, lbw_b in loras: if (lora_name.split('.')[-1]) not in folder_paths.supported_pt_extensions: lora_name = lora_name+".safetensors" diff --git a/web/js/easy/easy.js b/web/js/easy/easy.js index 9bdad01..1be1ba9 100644 --- a/web/js/easy/easy.js +++ b/web/js/easy/easy.js @@ -3,242 +3,11 @@ import { ComfyWidgets } from "/scripts/widgets.js"; import { $el } from "/scripts/ui.js"; import { api } from "/scripts/api.js"; -const BETTER_COMBOS_NODES = ["easy a1111Loader"] -const CONVERTED_TYPE = "converted-widget"; -const GET_CONFIG = Symbol(); -function hideWidget(node, widget, suffix = "") { - widget.origType = widget.type; - widget.origComputeSize = widget.computeSize; - widget.origSerializeValue = widget.serializeValue; - widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically - widget.type = CONVERTED_TYPE + suffix; - widget.serializeValue = () => { - // Prevent serializing the widget if we have no input linked - if (!node.inputs) { - return undefined; - } - let node_input = node.inputs.find((i) => i.widget?.name === widget.name); - - if (!node_input || !node_input.link) { - return undefined; - } - return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; - }; - - // Hide any linked widgets, e.g. seed+seedControl - if (widget.linkedWidgets) { - for (const w of widget.linkedWidgets) { - hideWidget(node, w, ":" + widget.name); - } - } -} -function deepEqual (obj1, obj2) { - if (typeof obj1 !== typeof obj2) { - return false - } - if (typeof obj1 !== 'object' || obj1 === null || obj2 === null) { - return obj1 === obj2 - } - const keys1 = Object.keys(obj1) - const keys2 = Object.keys(obj2) - if (keys1.length !== keys2.length) { - return false - } - for (let key of keys1) { - if (!deepEqual(obj1[key], obj2[key])) { - return false - } - } - return true -} -function convertToInput(node, widget, config) { - console.log('config:', config) - hideWidget(node, widget); - - const { type } = getWidgetType(config); - - // Add input and store widget config for creating on primitive node - const sz = node.size; - node.addInput(widget.name, type, { - widget: { name: widget.name, [GET_CONFIG]: () => config }, - }); - - for (const widget of node.widgets) { - widget.last_y += LiteGraph.NODE_SLOT_HEIGHT; - } - - // Restore original size but grow if needed - node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]); -} - -function getWidgetType(config) { - // Special handling for COMBO so we restrict links based on the entries - let type = config[0]; - if (type instanceof Array) { - type = "COMBO"; - } - return { type }; -} app.registerExtension({ name: "comfy.easyUse", init() { - // 刷新节点 - const easyReloadNode = function (node) { - const nodeType = node.constructor.type; - const origVals = node.properties.origVals || {}; - - const nodeTitle = origVals.title || node.title; - const nodeColor = origVals.color || node.color; - const bgColor = origVals.bgcolor || node.bgcolor; - const oldNode = node - const options = { - 'size': [...node.size], - 'color': nodeColor, - 'bgcolor': bgColor, - 'pos': [...node.pos] - } - - let inputLinks = [] - let outputLinks = [] - if(node.inputs){ - for (const input of node.inputs) { - if (input.link) { - const input_name = input.name - const input_slot = node.findInputSlot(input_name) - const input_node = node.getInputNode(input_slot) - const input_link = node.getInputLink(input_slot) - - inputLinks.push([input_link.origin_slot, input_node, input_name]) - } - } - } - if(node.outputs) { - for (const output of node.outputs) { - if (output.links) { - const output_name = output.name - - for (const linkID of output.links) { - const output_link = graph.links[linkID] - const output_node = graph._nodes_by_id[output_link.target_id] - outputLinks.push([output_name, output_node, output_link.target_slot]) - } - } - } - } - - app.graph.remove(node) - const newNode = app.graph.add(LiteGraph.createNode(nodeType, nodeTitle, options)); - - function handleLinks() { - // re-convert inputs - for (let w of oldNode.widgets) { - if (w.type === 'converted-widget') { - const WidgetToConvert = newNode.widgets.find((nw) => nw.name === w.name); - for (let i of oldNode.inputs) { - if (i.name === w.name) { - convertToInput(newNode, WidgetToConvert, i.widget); - } - } - } - } - // replace input and output links - for (let input of inputLinks) { - const [output_slot, output_node, input_name] = input; - output_node.connect(output_slot, newNode.id, input_name) - } - for (let output of outputLinks) { - const [output_name, input_node, input_slot] = output; - newNode.connect(output_name, input_node, input_slot) - } - } - - // fix widget values - let values = oldNode.widgets_values; - if (!values) { - newNode.widgets.forEach((newWidget, index) => { - const oldWidget = oldNode.widgets[index]; - if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) { - newWidget.value = oldWidget.value; - } - }); - handleLinks(); - return; - } - let pass = false - const isIterateForwards = values.length <= newNode.widgets.length; - let vi = isIterateForwards ? 0 : values.length - 1; - function evalWidgetValues(testValue, newWidg) { - if (testValue === true || testValue === false) { - if (newWidg.options?.on && newWidg.options?.off) { - return { value: testValue, pass: true }; - } - } else if (typeof testValue === "number") { - if (newWidg.options?.min <= testValue && testValue <= newWidg.options?.max) { - return { value: testValue, pass: true }; - } - } else if (newWidg.options?.values?.includes(testValue)) { - return { value: testValue, pass: true }; - } else if (newWidg.inputEl && typeof testValue === "string") { - return { value: testValue, pass: true }; - } - return { value: newWidg.value, pass: false }; - } - const updateValue = (wi) => { - const oldWidget = oldNode.widgets[wi]; - let newWidget = newNode.widgets[wi]; - if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) { - while ((isIterateForwards ? vi < values.length : vi >= 0) && !pass) { - let { value, pass } = evalWidgetValues(values[vi], newWidget); - if (pass && value !== null) { - newWidget.value = value; - break; - } - vi += isIterateForwards ? 1 : -1; - } - vi++ - if (!isIterateForwards) { - vi = values.length - (newNode.widgets.length - 1 - wi); - } - } - }; - if (isIterateForwards) { - for (let wi = 0; wi < newNode.widgets.length; wi++) { - updateValue(wi); - } - } else { - for (let wi = newNode.widgets.length - 1; wi >= 0; wi--) { - updateValue(wi); - } - } - handleLinks(); - }; - - // Nodes Menu - const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions; - LGraphCanvas.prototype.getNodeMenuOptions = function (node) { - const options = getNodeMenuOptions.apply(this, arguments); - node.setDirtyCanvas(true, true); - - options.splice(options.length - 1, 0, - { - content: "🔃Reload Node (EasyUse)", - callback: () => { - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { - easyReloadNode(node); - } else { - for (var i in graphcanvas.selected_nodes) { - easyReloadNode(graphcanvas.selected_nodes[i]); - } - } - } - } - ); - return options; - }; - // Canvas Menu const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions; LGraphCanvas.prototype.getCanvasMenuOptions = function () { diff --git a/web/js/easy/easyDynamicWidgets.js b/web/js/easy/easyDynamicWidgets.js index 031ed62..82773cd 100644 --- a/web/js/easy/easyDynamicWidgets.js +++ b/web/js/easy/easyDynamicWidgets.js @@ -19,7 +19,7 @@ function toggleWidget(node, widget, show = false, suffix = "") { } const origSize = node.size; - widget.type = show ? origProps[widget.name].origType : "esayHidden" + suffix; + widget.type = show ? origProps[widget.name].origType : "easyHidden" + suffix; widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4]; widget.linkedWidgets?.forEach(w => toggleWidget(node, w, ":" + widget.name, show)); @@ -438,10 +438,12 @@ app.registerExtension({ case "easy preSamplingAdvanced": case "easy preSamplingSdTurbo": case "easy preSamplingCascade": + case "easy preSamplingLayerDiffusion": case "easy fullkSampler": case "easy kSampler": case "easy kSamplerSDTurbo": case "easy kSamplerTiled": + case "easy kSamplerLayerDiffusion": case "easy kSamplerInpainting": case "easy kSamplerDownscaleUnet": case "easy fullCascadeKSampler": @@ -753,7 +755,7 @@ app.registerExtension({ }; } - if (["easy seed", "easy latentNoisy", "easy wildcards", "easy preSampling", "easy preSamplingAdvanced", "easy preSamplingSdTurbo", "easy preSamplingCascade", "easy preSamplingDynamicCFG", "easy fullkSampler", "easy fullCascadeKSampler"].includes(nodeData.name)) { + if (["easy seed", "easy latentNoisy", "easy wildcards", "easy preSampling", "easy preSamplingAdvanced", "easy preSamplingSdTurbo", "easy preSamplingCascade", "easy preSamplingDynamicCFG", "easy preSamplingLayerDiffusion", "easy fullkSampler", "easy fullCascadeKSampler"].includes(nodeData.name)) { const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = async function () { onNodeCreated ? onNodeCreated.apply(this, []) : undefined; diff --git a/web/js/easy/easyExtraMenu.js b/web/js/easy/easyExtraMenu.js new file mode 100644 index 0000000..879e8d7 --- /dev/null +++ b/web/js/easy/easyExtraMenu.js @@ -0,0 +1,508 @@ +import {app} from "/scripts/app.js"; + +const loaders = ['easy fullLoader', 'easy a1111Loader', 'easy comfyLoader'] +const preSampling = ['easy preSampling', 'easy preSamplingAdvanced', 'easy preSamplingDynamicCFG', '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 positive_prompt = ['easy positive', 'easy wildcards'] +const widgetMapping = { + "positive_prompt":{ + "text": "positive", + "positive": "text" + }, + "loaders":{ + "ckpt_name": "ckpt_name", + "vae_name": "vae_name", + "clip_skip": "clip_skip", + "lora_name": "lora_name", + "resolution": "resolution", + "empty_latent_width": "empty_latent_width", + "empty_latent_height": "empty_latent_height", + "positive": "positive", + "negative": "negative", + "batch_size": "batch_size", + "a1111_prompt_style": "a1111_prompt_style" + }, + "preSampling":{ + "steps": "steps", + "cfg": "cfg", + "sampler_name": "sampler_name", + "scheduler": "scheduler", + "denoise": "denoise", + "seed_num": "seed_num" + }, + "kSampler":{ + "image_output": "image_output", + "save_prefix": "save_prefix", + "link_id": "link_id" + }, + "controlnet":{ + "control_net_name":"control_net_name", + "strength": ["strength", "cn_strength"], + "scale_soft_weights": ["scale_soft_weights","cn_soft_weights"], + "cn_strength": ["strength", "cn_strength"], + "cn_soft_weights": ["scale_soft_weights","cn_soft_weights"], + } +} +const inputMapping = { + "loaders":{ + "optional_lora_stack": "optional_lora_stack", + "positive": "positive", + "negative": "negative" + }, + "preSampling":{ + "pipe": "pipe", + "image_to_latent": "image_to_latent", + "latent": "latent" + }, + "kSampler":{ + "pipe": "pipe", + "model": "model" + }, + "controlnet":{ + "pipe": "pipe", + "image": "image", + "image_kps": "image_kps", + "control_net": "control_net", + "positive": "positive", + "negative": "negative", + "mask": "mask" + }, + "positive_prompt":{ + + } +}; + +const outputMapping = { + "loaders":{ + "pipe": "pipe", + "model": "model", + "vae": "vae", + "clip": null, + "positive": null, + "negative": null, + "latent": null, + }, + "preSampling":{ + "pipe":"pipe" + }, + "kSampler":{ + "pipe": "pipe", + "image": "image" + }, + "controlnet":{ + "pipe": "pipe", + "positive": "positive", + "negative": "negative" + }, + "positive_prompt":{ + "text": "positive", + "positive": "text" + }, +}; + +// 替换节点 +function replaceNode(oldNode, newNodeName, type) { + const newNode = LiteGraph.createNode(newNodeName); + if (!newNode) { + return; + } + app.graph.add(newNode); + + newNode.pos = oldNode.pos.slice(); + newNode.size = oldNode.size.slice(); + + oldNode.widgets.forEach(widget => { + if(widgetMapping[type][widget.name]){ + const newName = widgetMapping[type][widget.name]; + if (newName) { + const newWidget = findWidgetByName(newNode, newName); + if (newWidget) { + newWidget.value = widget.value; + if(widget.name == 'seed_num'){ + newWidget.linkedWidgets[0].value = widget.linkedWidgets[0].value + } + if(widget.type == 'converted-widget'){ + convertToInput(newNode, newWidget, widget); + } + } + } + } + + }); + + if(oldNode.inputs){ + oldNode.inputs.forEach((input, index) => { + if (input && input.link && inputMapping[type][input.name]) { + const newInputName = inputMapping[type][input.name]; + // If the new node does not have this output, skip + if (newInputName === null) { + return; + } + const newInputIndex = newNode.findInputSlot(newInputName); + if (newInputIndex !== -1) { + const originLinkInfo = oldNode.graph.links[input.link]; + if (originLinkInfo) { + const originNode = oldNode.graph.getNodeById(originLinkInfo.origin_id); + if (originNode) { + originNode.connect(originLinkInfo.origin_slot, newNode, newInputIndex); + } + } + } + } + }); + } + + if(oldNode.outputs){ + oldNode.outputs.forEach((output, index) => { + if (output && output.links && outputMapping[type] && outputMapping[type][output.name]) { + const newOutputName = outputMapping[type][output.name]; + // If the new node does not have this output, skip + if (newOutputName === null) { + return; + } + const newOutputIndex = newNode.findOutputSlot(newOutputName); + if (newOutputIndex !== -1) { + output.links.forEach(link => { + const targetLinkInfo = oldNode.graph.links[link]; + if (targetLinkInfo) { + const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id); + if (targetNode) { + newNode.connect(newOutputIndex, targetNode, targetLinkInfo.target_slot); + } + } + }); + } + } + }); + } + + + // Remove old node + app.graph.remove(oldNode); + + // Remove others + if(newNode.type == 'easy fullkSampler'){ + const link_output_id = newNode.outputs[0].links + if(link_output_id && link_output_id[0]){ + const nodes = app.graph._nodes + const node = nodes.find(cate=> cate.inputs && cate.inputs[0] && cate.inputs[0]['link'] == link_output_id[0]) + if(node){ + app.graph.remove(node); + } + } + }else if(preSampling.includes(newNode.type)){ + const link_output_id = newNode.outputs[0].links + if(!link_output_id || !link_output_id[0]){ + const ksampler = LiteGraph.createNode('easy kSampler'); + app.graph.add(ksampler); + ksampler.pos = newNode.pos.slice(); + ksampler.pos[0] = ksampler.pos[0] + newNode.size[0] + 20; + const newInputIndex = newNode.findInputSlot('pipe'); + if (newInputIndex !== -1) { + if (newNode) { + newNode.connect(0, ksampler, newInputIndex); + } + } + } + } + + // autoHeight + newNode.setSize([newNode.size[0], newNode.computeSize()[1]]); +} + +export function findWidgetByName(node, widgetName) { + return node.widgets.find(widget => typeof widgetName == 'object' ? widgetName.includes(widget.name) : widget.name === widgetName); +} +function replaceNodeMenuCallback(currentNode, targetNodeName, type) { + return function() { + replaceNode(currentNode, targetNodeName, type); + }; +} +const addMenuHandler = (nodeType, cb)=> { + const getOpts = nodeType.prototype.getExtraMenuOptions; + nodeType.prototype.getExtraMenuOptions = function () { + const r = getOpts.apply(this, arguments); + cb.apply(this, arguments); + return r; + }; +} +const addMenu = (content, type, nodes_include, nodeType, has_submenu=true) => { + addMenuHandler(nodeType, function (_, options) { + options.unshift({ + content: content, + has_submenu: has_submenu, + callback: (value, options, e, menu, node) => showSwapMenu(value, options, e, menu, node, type, nodes_include) + }) + }) +} +const showSwapMenu = (value, options, e, menu, node, type, nodes_include) => { + const swapOptions = []; + nodes_include.map(cate=>{ + if (node.type !== cate) { + swapOptions.push({ + content: `${cate}`, + callback: replaceNodeMenuCallback(node, cate, type) + }); + } + }) + new LiteGraph.ContextMenu(swapOptions, { + event: e, + callback: null, + parentMenu: menu, + node: node + }); + return false; +} + +// 重载节点 +const CONVERTED_TYPE = "converted-widget"; +const GET_CONFIG = Symbol(); + +function hideWidget(node, widget, suffix = "") { + widget.origType = widget.type; + widget.origComputeSize = widget.computeSize; + widget.origSerializeValue = widget.serializeValue; + widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically + widget.type = CONVERTED_TYPE + suffix; + widget.serializeValue = () => { + // Prevent serializing the widget if we have no input linked + if (!node.inputs) { + return undefined; + } + let node_input = node.inputs.find((i) => i.widget?.name === widget.name); + + if (!node_input || !node_input.link) { + return undefined; + } + return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; + }; + + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + hideWidget(node, w, ":" + widget.name); + } + } +} +function deepEqual (obj1, obj2) { + if (typeof obj1 !== typeof obj2) { + return false + } + if (typeof obj1 !== 'object' || obj1 === null || obj2 === null) { + return obj1 === obj2 + } + const keys1 = Object.keys(obj1) + const keys2 = Object.keys(obj2) + if (keys1.length !== keys2.length) { + return false + } + for (let key of keys1) { + if (!deepEqual(obj1[key], obj2[key])) { + return false + } + } + return true +} +function convertToInput(node, widget, config) { + console.log('config:', config) + hideWidget(node, widget); + + const { type } = getWidgetType(config); + + // Add input and store widget config for creating on primitive node + const sz = node.size; + node.addInput(widget.name, type, { + widget: { name: widget.name, [GET_CONFIG]: () => config }, + }); + + for (const widget of node.widgets) { + widget.last_y += LiteGraph.NODE_SLOT_HEIGHT; + } + + // Restore original size but grow if needed + node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]); +} + +function getWidgetType(config) { + // Special handling for COMBO so we restrict links based on the entries + let type = config[0]; + if (type instanceof Array) { + type = "COMBO"; + } + return { type }; +} + +const reloadNode = function (node) { + const nodeType = node.constructor.type; + const origVals = node.properties.origVals || {}; + + const nodeTitle = origVals.title || node.title; + const nodeColor = origVals.color || node.color; + const bgColor = origVals.bgcolor || node.bgcolor; + const oldNode = node + const options = { + 'size': [...node.size], + 'color': nodeColor, + 'bgcolor': bgColor, + 'pos': [...node.pos] + } + + let inputLinks = [] + let outputLinks = [] + if(node.inputs){ + for (const input of node.inputs) { + if (input.link) { + const input_name = input.name + const input_slot = node.findInputSlot(input_name) + const input_node = node.getInputNode(input_slot) + const input_link = node.getInputLink(input_slot) + + inputLinks.push([input_link.origin_slot, input_node, input_name]) + } + } + } + if(node.outputs) { + for (const output of node.outputs) { + if (output.links) { + const output_name = output.name + + for (const linkID of output.links) { + const output_link = graph.links[linkID] + const output_node = graph._nodes_by_id[output_link.target_id] + outputLinks.push([output_name, output_node, output_link.target_slot]) + } + } + } + } + + app.graph.remove(node) + const newNode = app.graph.add(LiteGraph.createNode(nodeType, nodeTitle, options)); + + function handleLinks() { + // re-convert inputs + for (let w of oldNode.widgets) { + if (w.type === 'converted-widget') { + const WidgetToConvert = newNode.widgets.find((nw) => nw.name === w.name); + for (let i of oldNode.inputs) { + if (i.name === w.name) { + convertToInput(newNode, WidgetToConvert, i.widget); + } + } + } + } + // replace input and output links + for (let input of inputLinks) { + const [output_slot, output_node, input_name] = input; + output_node.connect(output_slot, newNode.id, input_name) + } + for (let output of outputLinks) { + const [output_name, input_node, input_slot] = output; + newNode.connect(output_name, input_node, input_slot) + } + } + + // fix widget values + let values = oldNode.widgets_values; + if (!values) { + newNode.widgets.forEach((newWidget, index) => { + const oldWidget = oldNode.widgets[index]; + if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) { + newWidget.value = oldWidget.value; + } + }); + handleLinks(); + return; + } + let pass = false + const isIterateForwards = values.length <= newNode.widgets.length; + let vi = isIterateForwards ? 0 : values.length - 1; + function evalWidgetValues(testValue, newWidg) { + if (testValue === true || testValue === false) { + if (newWidg.options?.on && newWidg.options?.off) { + return { value: testValue, pass: true }; + } + } else if (typeof testValue === "number") { + if (newWidg.options?.min <= testValue && testValue <= newWidg.options?.max) { + return { value: testValue, pass: true }; + } + } else if (newWidg.options?.values?.includes(testValue)) { + return { value: testValue, pass: true }; + } else if (newWidg.inputEl && typeof testValue === "string") { + return { value: testValue, pass: true }; + } + return { value: newWidg.value, pass: false }; + } + const updateValue = (wi) => { + const oldWidget = oldNode.widgets[wi]; + let newWidget = newNode.widgets[wi]; + if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) { + while ((isIterateForwards ? vi < values.length : vi >= 0) && !pass) { + let { value, pass } = evalWidgetValues(values[vi], newWidget); + if (pass && value !== null) { + newWidget.value = value; + break; + } + vi += isIterateForwards ? 1 : -1; + } + vi++ + if (!isIterateForwards) { + vi = values.length - (newNode.widgets.length - 1 - wi); + } + } + }; + if (isIterateForwards) { + for (let wi = 0; wi < newNode.widgets.length; wi++) { + updateValue(wi); + } + } else { + for (let wi = newNode.widgets.length - 1; wi >= 0; wi--) { + updateValue(wi); + } + } + handleLinks(); +}; + + +app.registerExtension({ + name: "comfy.easyUse.extraMenu", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // 刷新节点 + addMenuHandler(nodeType, function (_, options) { + options.unshift({ + content: "🔃 Reload Node", + callback: (value, options, e, menu, node) => { + let graphcanvas = LGraphCanvas.active_canvas; + if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { + reloadNode(node); + } else { + for (let i in graphcanvas.selected_nodes) { + reloadNode(graphcanvas.selected_nodes[i]); + } + } + } + }) + }) + // Swap提示词 + if (positive_prompt.includes(nodeData.name)) { + addMenu("↪️ Swap EasyPrompt", 'positive_prompt', positive_prompt, nodeType) + } + // Swap加载器 + if (loaders.includes(nodeData.name)) { + addMenu("↪️ Swap EasyLoader", 'loaders', loaders, nodeType) + } + // Swap预采样器 + if (preSampling.includes(nodeData.name)) { + addMenu("↪️ Swap EasyPreSampling", 'preSampling', preSampling, nodeType) + } + // Swap kSampler + if (kSampler.includes(nodeData.name)) { + addMenu("↪️ Swap EasyKSampler", 'preSampling', kSampler, nodeType) + } + // Swap ControlNet + if (controlnet.includes(nodeData.name)) { + addMenu("↪️ Swap EasyControlnet", 'controlnet', controlnet, nodeType) + } + } +}); + diff --git a/web/js/easy/easyInterface.js b/web/js/easy/easyInterface.js index d680303..270e4a3 100644 --- a/web/js/easy/easyInterface.js +++ b/web/js/easy/easyInterface.js @@ -125,7 +125,6 @@ try{ app.ui.settings.load() } } - console.log(theme_name) // 判断主题为黑曜石时改变扩展UI if(['"custom_obsidian"','"custom_obsidian_dark"'].includes(theme_name)){ // canvas diff --git a/web/js/easy/easySaveImage.js b/web/js/easy/easySaveImage.js index 84e8c63..88831f4 100644 --- a/web/js/easy/easySaveImage.js +++ b/web/js/easy/easySaveImage.js @@ -1,10 +1,12 @@ import { app } from "/scripts/app.js"; import { applyTextReplacements } from "/scripts/utils.js"; +const extraNodes = ["easy imageSave", "easy fullkSampler", "easy kSampler", "easy kSamplerTiled","easy kSamplerInpainting", "easy kSamplerDownscaleUnet", "easy kSamplerSDTurbo","easy detailerFix"] + app.registerExtension({ name: "Comfy.Easy.SaveImageExtraOutput", async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (["easy imageSave", "easy fullkSampler", "easy kSampler", "easy kSamplerTiled","easy kSamplerInpainting", "easy kSamplerDownscaleUnet", "easy kSamplerSDTurbo","easy detailerFix"].includes(nodeData.name)) { + if (extraNodes.includes(nodeData.name)) { const onNodeCreated = nodeType.prototype.onNodeCreated; // When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R nodeType.prototype.onNodeCreated = function () {