From f6b5f5c99cb99bfd5eb59cdfdb9495b3cd258f27 Mon Sep 17 00:00:00 2001 From: yolain Date: Fri, 26 Apr 2024 11:55:12 +0800 Subject: [PATCH] Upgrade to v1.1.6 --- README.en.md | 53 ++++++++--- README.md | 35 +++++-- __init__.py | 4 +- py/easyNodes.py | 97 +++++++++++++++++++- py/image.py | 45 ++++++++- py/libs/image.py | 29 +++--- py/libs/styleAlign.py | 148 ++++++++++++++++++++++++++++++ py/libs/utils.py | 7 +- py/logic.py | 29 ++++++ web/css/chooser.css | 30 ++++++ web/css/index.css | 3 +- web/js/common/i18n.js | 3 + web/js/easy/easyDynamicWidgets.js | 2 +- web/js/easy/easyExtraMenu.js | 16 +++- web/js/easy/easyInterface.js | 1 + web/js/image_chooser/chooser.js | 68 +++++++++++++- web/js/image_chooser/preview.js | 6 +- 17 files changed, 520 insertions(+), 56 deletions(-) create mode 100644 py/libs/styleAlign.py create mode 100644 web/css/chooser.css diff --git a/README.en.md b/README.en.md index 43a9127..a816ee7 100644 --- a/README.en.md +++ b/README.en.md @@ -33,6 +33,17 @@ - ## Changelog +**v1.1.6 (2024/4/26)** + +- Added `easy styleAlignedBatchAlign` - Credit of [style_aligned_comfy](https://github.com/brianfitzgerald/style_aligned_comfy) +- Added `easy ckptNames` +- Added `easy controlnetNames` +- Added `easy imagesSplitimage` - Batch images split into single images +- Added `easy imageCount` - Get Image Count +- Added `easy textSwitch` - Text Switch +- `easy kSampler` & `easy fullkSampler` added **Preview&Choose** to **image_output** widget + + **v1.1.5 (2024/4/24)** - Rewrite `easy cleanGPUUsed` - the memory usage of the comfyUI can to be cleared @@ -60,7 +71,7 @@ - Added `easy promptConcat` - `easy wildcards` Added **multiline_mode** -**v1.1.2 (39c5ccf)** +**v1.1.2** - Optimized some of the recommended nodes for slots related to EasyUse - Added **Enable ContextMenu Auto Nest Subdirectories** The setting item is enabled by default, and it can be classified into subdirectories, checkpoints and loras previews @@ -76,7 +87,7 @@ - Fixed layerDiffuse xyplot bug - `easy kSamplerInpainting` add *additional* widget,you can choose 'Differential Diffusion' or 'Only InpaintModelConditioning' -**v1.1.1 (2024/3/16)** +**v1.1.1** - The issue that the seed is 0 when a node with a seed control is added and **control before generate** is fixed for the first time run queue prompt. - `easy preSamplingAdvanced` Added **return_with_leftover_noise** @@ -86,7 +97,7 @@ - Remove forced **control_before_generate** settings。 If you want to use control_before_generate, change widget_value_control_mode to before in system settings - Added `easy imageRemBg` - The default is BriaAI's RMBG-1.4 model, which removes the background effect more and faster -**v1.1.0 (d5ff84e)** +**v1.1.0** - Added `easy imageSplitList` - to split every N images - Added `easy preSamplingDiffusionADDTL` - It can modify foreground、background or blended additional prompt @@ -102,15 +113,18 @@ - Fixed `easy instantIDApply` mask not input right -**v1.0.9 (ff1add1)** +
+v1.0.9 - Fixed the error when ComfyUI-Impack-Pack and ComfyUI_InstantID were not installed - Fixed `easy pipeIn` - 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 +
-**v1.0.8 (f28cbf7)** +
+v1.0.8 - `easy cascadeLoader` stage_c and stage_b support the checkpoint model (Download [checkpoints](https://huggingface.co/stabilityai/stable-cascade/tree/main/comfyui_checkpoints) models) - `easy styleSelector` The search box is modified to be case-insensitive @@ -120,8 +134,10 @@ - Fixed the error of SDXLClipModel in ComfyUI revision 2016[c2cb8e88] and above (the revision number was judged to be compatible with the old revision) - Fixed `easy detailerFix` generation error when batch size is greater than 1 - Optimize the code, reduce a lot of redundant code and improve the running speed +
-**v1.0.7 (2024-02-19)** +
+v1.0.7 - Added `easy cascadeLoader` - stable cascade Loader - Added `easy preSamplingCascade` - stable cascade preSampling Settings @@ -129,22 +145,28 @@ - Added `easy cascadeKSampler` - stable cascade stage-c ksampler simple - - Optimize the image to image[Example](https://github.com/yolain/ComfyUI-Easy-Use/blob/main/README.en.md#image-to-image) +
-**v1.0.6** +
+v1.0.6 - Added `easy XYInputs: Checkpoint` - Added `easy XYInputs: Lora` - `easy seed` can manually switch the random seed when increasing the fixed seed value - Fixed `easy fullLoader` and all loaders to automatically adjust the node size when switching LoRa - Removed the original ttn image saving logic and adapted to the default image saving format extension of ComfyUI +
-- **v1.0.5** +
+v1.0.5 - Added `easy isSDXL` - Added prompt word control on `easy svdLoader`, which can be used with open_clip model - Added **populated_text** on `easy wildcards`, wildcard populated text can be output +
-**v1.0.4** +
+v1.0.4 - `easy showAnything` added support for converting other types (e.g., tensor conditions, images, etc.) - Added `easy showLoaderSettingsNames` can display the model and VAE name in the output loader assembly @@ -164,9 +186,10 @@ - Changing the first-time install node package no longer automatically replaces the theme, you need to manually adjust and refresh the page - `easy imageSave` added **only_preivew** - Adjust the `easy latentCompositeMaskedWithCond` node +
- -**v1.0.3** +
+v1.0.3 - Added `easy stylesSelector` - Added **scale_soft_weights** in `easy controlnetLoader` and `easy controlnetLoaderADV` @@ -184,9 +207,10 @@ - Adjust the UI theme, divided into two sets of styles: the official default background and the dark black background, which can be switched in the color palette in the settings - Modify the styles path to be compatible with other environments +
- -**v1.0.2** +
+v1.0.2 - Added `easy XYPlotAdvanced` and some nodes about `easy XYInputs` - Added **Alt+1-Alt+9** Shortcut keys to quickly paste node presets for Node templates (corresponding to 1~9 sequences) @@ -203,7 +227,7 @@ - Removed `easy imageRemBg` - Remove the introductory diagram and workflow files from the package to reduce the package size - Replaced the font file used in the generation of XY diagrams - +
v1.0.1 @@ -298,6 +322,7 @@ Disclaimer: Opened source was not easy. I have a lot of respect for the contribu | easy preSamplingLayerDiffusion | [ComfyUI-layerdiffusion](https://github.com/huchenlei/ComfyUI-layerdiffusion) | LayeredDiffusionApply... | | easy dynamiCrafterLoader | [ComfyUI-layerdiffusion](https://github.com/ExponentialML/ComfyUI_Native_DynamiCrafter) | Apply Dynamicrafter | | easy imageChooser | [cg-image-picker](https://github.com/chrisgoringe/cg-image-picker) | Preview Chooser | +| easy styleAlignedBatchAlign | [style_aligned_comfy](https://github.com/chrisgoringe/cg-image-picker) | styleAlignedBatchAlign | ## Workflow Examples diff --git a/README.md b/README.md index 085184e..6087e5d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,17 @@ ## 更新日志 -**v1.1.5 (2024/4/24)** +**v1.1.6 (2024/4/26)** + +- 增加 `easy styleAlignedBatchAlign` - 风格对齐 [style_aligned_comfy](https://github.com/brianfitzgerald/style_aligned_comfy) +- 增加 `easy ckptNames` +- 增加 `easy controlnetNames` +- 增加 `easy imagesSplitimage` - 批次图像拆分单张 +- 增加 `easy imageCount` - 图像数量 +- 增加 `easy textSwitch` - 文字切换 +- `easy kSampler` 和 `easy fullkSampler` 的 **image_output** 增加 **Preview&Choose**选项 + +**v1.1.5** - 重写 `easy cleanGPUUsed` - 可强制清理comfyUI的模型显存占用 - 增加 `easy humanSegmentation` - 多类分割、人像分割 @@ -47,7 +57,7 @@ - 增加 `easy imageInterrogator` - 图像反推 - 增加 `easy stableDiffusion3API` - 简易的Stable Diffusion 3 多账号API节点 -**v1.1.4 (2024/4/13)** +**v1.1.4** - 增加 `easy imageChooser` - 从[cg-image-picker](https://github.com/chrisgoringe/cg-image-picker)简化的图片选择器 - 增加 `easy preSamplingCustom` - 自定义预采样,可支持cosXL-edit @@ -56,7 +66,7 @@ - 修复 `easy preSamplingNoiseIn`、`easy latentNoisy`、`east Unsampler` 以兼容ComfyUI Revision>=2098 [0542088e] 以上版本 - 修复 FooocusInpaint修改ModelPatcher计算权重引发的问题,理应在生成model后重置ModelPatcher为默认值 -**v1.1.3 (2024/4/4)** +**v1.1.3** - `easy ipadapterApply` 增加 **COMPOSITION** 预置项 - 增加 对[ResAdapter](https://huggingface.co/jiaxiangc/res-adapter) lora模型 的加载支持 @@ -66,7 +76,7 @@ - `easy wildcards` 增加 **multiline_mode**属性 - 增加 当节点需要下载模型时,若huggingface连接超时,会切换至镜像地址下载模型 -**v1.1.2 (39c5ccf)** +**v1.1.2** - 改写 EasyUse 相关节点的部分插槽推荐节点 - 增加 **启用上下文菜单自动嵌套子目录** 设置项,默认为启用状态,可分类子目录及checkpoints、loras预览图 @@ -82,7 +92,7 @@ - 修复 `easy pipeEdit` 提示词输入lora时报错 - 修复 layerDiffuse xyplot相关bug -**v1.1.1 (5c8af8f)** +**v1.1.1** - 修复首次添加含seed的节点且当前模式为control_before_generate时,seed为0的问题 - `easy preSamplingAdvanced` 增加 **return_with_leftover_noise** @@ -93,7 +103,7 @@ - 去除强制**control_before_generate**设定 - 增加 `easy imageRemBg` - 默认为BriaAI的RMBG-1.4模型, 移除背景效果更加,速度更快 -**v1.1.0 (d5ff84e)** +**v1.1.0** - 增加 `easy imageSplitList` - 拆分每 N 张图像 - 增加 `easy preSamplingDiffusionADDTL` - 可配置前景、背景、blended的additional_prompt等 @@ -107,15 +117,18 @@ - 修复 `easy instantIDApply` mask 未传入正确值 - 修复 在 非a1111提示词风格下 BREAK 不生效的问题 -**v1.0.9 (ff1add1)** +
+v1.0.9 - 修复未安装 ComfyUI-Impack-Pack 和 ComfyUI_InstantID 时报错 - 修复 `easy pipeIn` - pipe设为可不必选 - 增加 `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` 在替换负面提示词时报错 +
-**v1.0.8 (f28cbf7)** +
+v1.0.8 - `easy cascadeLoader` stage_c 与 stage_b 支持checkpoint模型 (需要下载[checkpoints](https://huggingface.co/stabilityai/stable-cascade/tree/main/comfyui_checkpoints)) - `easy styleSelector` 搜索框修改为不区分大小写匹配 @@ -129,13 +142,16 @@ (翻译对照已由 [AIGODLIKE-COMFYUI-TRANSLATION](https://github.com/AIGODLIKE/AIGODLIKE-ComfyUI-Translation) 统一维护啦! 首次下载或者版本较早的朋友请更新 AIGODLIKE-COMFYUI-TRANSLATION 和本节点包至最新版本。) +
-**v1.0.7** +
+v1.0.7 - 增加 `easy cascadeLoader` - stable cascade 加载器 - 增加 `easy preSamplingCascade` - stabled cascade stage_c 预采样参数 - 增加 `easy fullCascadeKSampler` - stable cascade stage_c 完整版采样器 - 增加 `easy cascadeKSampler` - stable cascade stage-c ksampler simple +
v1.0.6 @@ -310,6 +326,7 @@ | easy preSamplingLayerDiffusion | [ComfyUI-layerdiffusion](https://github.com/huchenlei/ComfyUI-layerdiffusion) | LayeredDiffusionApply等 | | easy dynamiCrafterLoader | [ComfyUI-layerdiffusion](https://github.com/ExponentialML/ComfyUI_Native_DynamiCrafter) | Apply Dynamicrafter | | easy imageChooser | [cg-image-picker](https://github.com/chrisgoringe/cg-image-picker) | Preview Chooser | +| easy styleAlignedBatchAlign | [style_aligned_comfy](https://github.com/chrisgoringe/cg-image-picker) | styleAlignedBatchAlign | ## 示例 diff --git a/__init__.py b/__init__.py index 75110db..d75c3ea 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,5 @@ +__version__ = "1.1.6" + import os import glob import folder_paths @@ -85,4 +87,4 @@ WEB_DIRECTORY = "./web" __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', "WEB_DIRECTORY"] -print('\033[34mComfy-Easy-Use (v1.1.5): \033[92mLoaded\033[0m') \ No newline at end of file +print(f'\033[34mComfy-Easy-Use v{__version__}: \033[92mLoaded\033[0m') \ No newline at end of file diff --git a/py/easyNodes.py b/py/easyNodes.py index ccf7f83..7669810 100644 --- a/py/easyNodes.py +++ b/py/easyNodes.py @@ -22,7 +22,7 @@ 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, AlwaysEqualProxy +from .libs.utils import find_wildcards_seed, is_linked_styles_selector, easySave, get_local_filepath, add_folder_path_and_extensions, AlwaysEqualProxy, get_sd_version from .libs.loader import easyLoader from .libs.sampler import easySampler from .libs.xyplot import easyXYPlot @@ -819,6 +819,39 @@ class globalSeed: #---------------------------------------------------------------加载器 开始----------------------------------------------------------------------# +class setCkptName: + @classmethod + def INPUT_TYPES(cls): + return {"required": { + "ckpt_name": (folder_paths.get_filename_list("checkpoints"),), + } + } + + RETURN_TYPES = (AlwaysEqualProxy('*'),) + RETURN_NAMES = ("ckpt_name",) + FUNCTION = "set_name" + CATEGORY = "EasyUse/Utils" + + def set_name(self, ckpt_name): + return (ckpt_name,) + +class setControlName: + + @classmethod + def INPUT_TYPES(cls): + return {"required": { + "controlnet_name": (folder_paths.get_filename_list("controlnet"),), + } + } + + RETURN_TYPES = (AlwaysEqualProxy('*'),) + RETURN_NAMES = ("controlnet_name",) + FUNCTION = "set_name" + CATEGORY = "EasyUse/Utils" + + def set_name(self, controlnet_name): + return (controlnet_name,) + # 简易加载器完整 class fullLoader: @@ -2084,6 +2117,27 @@ class fooocusInpaintLoader: return ((inpaint_head_model, inpaint_lora),) #---------------------------------------------------------------适配器 开始----------------------------------------------------------------------# +from .libs.styleAlign import styleAlignBatch, SHARE_NORM_OPTIONS, SHARE_ATTN_OPTIONS +class styleAlignedBatchAlign: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "share_norm": (SHARE_NORM_OPTIONS,), + "share_attn": (SHARE_ATTN_OPTIONS,), + "scale": ("FLOAT", {"default": 1, "min": 0, "max": 1.0, "step": 0.1}), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "align" + CATEGORY = "EasyUse/Adapter" + + def align(self, model, share_norm, share_attn, scale): + return (styleAlignBatch(model, share_norm, share_attn, scale),) + def insightface_loader(provider): try: from insightface.app import FaceAnalysis @@ -3236,7 +3290,7 @@ class samplerCustomSettings: "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), "cfg_negative": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), "sampler_name": (comfy.samplers.KSampler.SAMPLERS + ['inversed_euler'],), - "scheduler": (comfy.samplers.KSampler.SCHEDULERS + ['karrasADV','exponentialADV','polyExponential','sdturbo','vp'],), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS + ['karrasADV','exponentialADV','polyExponential', 'sdturbo', 'vp'],), "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), "sigma_max": ("FLOAT", {"default": 14.614642, "min": 0.0, "max": 1000.0, "step": 0.01, "round": False}), "sigma_min": ("FLOAT", {"default": 0.0291675, "min": 0.0, "max": 1000.0, "step": 0.01, "round": False}), @@ -3327,6 +3381,7 @@ class samplerCustomSettings: return (ksampler,) def settings(self, pipe, guider, cfg, cfg_negative, sampler_name, scheduler, steps, sigma_max, sigma_min, rho, beta_d, beta_min, eps_s, flip_sigmas, denoise, add_noise, seed, image_to_latent=None, latent=None, optional_sampler=None, optional_sigmas=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + # 图生图转换 vae = pipe["vae"] model = pipe["model"] @@ -3334,6 +3389,7 @@ class samplerCustomSettings: negative = pipe['negative'] batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 _guider, sigmas = None, None + print(get_sd_version(model)) if image_to_latent is not None: if guider == "IP2P+DualCFG": positive, negative, latent = self.ip2p(pipe['positive'], pipe['negative'], vae, image_to_latent) @@ -3384,6 +3440,8 @@ class samplerCustomSettings: sigmas, = PolyexponentialScheduler().get_sigmas(steps, sigma_max, sigma_min, rho) elif scheduler == 'sdturbo': sigmas, = SDTurboScheduler().get_sigmas(model, steps, denoise) + elif scheduler == 'alignYourSteps': + pass else: sigmas, = BasicScheduler().get_sigmas(model, scheduler, steps, denoise) @@ -3937,6 +3995,7 @@ class dynamicThresholdingFull: #---------------------------------------------------------------采样器 开始---------------------------------------------------------------------- # 完整采样器 +from .libs.chooser import ChooserMessage, ChooserCancelled class samplerFull(LayerDiffuse): def __init__(self): @@ -3951,7 +4010,7 @@ class samplerFull(LayerDiffuse): "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), - "image_output": (["Hide", "Preview", "Save", "Hide/Save", "Sender", "Sender/Save"],), + "image_output": (["Hide", "Preview", "Preview&Choose", "Save", "Hide/Save", "Sender", "Sender/Save"],), "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), "save_prefix": ("STRING", {"default": "ComfyUI"}), }, @@ -4121,6 +4180,26 @@ class samplerFull(LayerDiffuse): del pipe + if image_output == 'Preview&Choose': + if my_unique_id not in ChooserMessage.stash: + ChooserMessage.stash[my_unique_id] = {} + my_stash = ChooserMessage.stash[my_unique_id] + + PromptServer.instance.send_sync("easyuse-image-choose", {"id": my_unique_id, "urls": results}) + # wait for selection + try: + selections = ChooserMessage.waitForMessage(my_unique_id, asList=True) + new_images = [new_images[x] for x in selections if x >= 0] if len(selections) > 1 else [0] + samp_images = [samp_images[x] for x in selections if x >= 0] if len(selections) > 1 else [0] + except ChooserCancelled: + raise comfy.model_management.InterruptProcessingException() + + new_pipe['images'] = new_images + new_pipe['samp_images'] = samp_images + + return {"ui": {"images": results}, + "result": sampler.get_output(new_pipe,)} + if image_output in ("Hide", "Hide/Save"): return {"ui": {}, "result": sampler.get_output(new_pipe,)} @@ -4272,7 +4351,7 @@ class samplerSimple: def INPUT_TYPES(cls): return {"required": {"pipe": ("PIPE_LINE",), - "image_output": (["Hide", "Preview", "Save", "Hide/Save", "Sender", "Sender/Save"],{"default": "Preview"}), + "image_output": (["Hide", "Preview", "Preview&Choose", "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"}), }, @@ -6946,6 +7025,7 @@ NODE_CLASS_MAPPINGS = { "easy ipadapterStyleComposition": ipadapterStyleComposition, "easy instantIDApply": instantIDApply, "easy instantIDApplyADV": instantIDApplyAdvanced, + "easy styleAlignedBatchAlign": styleAlignedBatchAlign, # Inpaint 内补 "easy fooocusInpaintLoader": fooocusInpaintLoader, # latent 潜空间 @@ -7009,6 +7089,9 @@ NODE_CLASS_MAPPINGS = { "dynamicThresholdingFull": dynamicThresholdingFull, # api 相关 "easy stableDiffusion3API": stableDiffusion3API, + # utils + "easy ckptNames": setCkptName, + "easy controlnetNames": setControlName, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -7048,6 +7131,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "easy ipadapterApplyFromParams": "Easy Apply IPAdapter (From Params)", "easy instantIDApply": "Easy Apply InstantID", "easy instantIDApplyADV": "Easy Apply InstantID (Advanced)", + "easy styleAlignedBatchAlign": "Easy Apply StyleAlign", # Inpaint 内补 "easy fooocusInpaintLoader": "Load Fooocus Inpaint", # latent 潜空间 @@ -7111,4 +7195,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "dynamicThresholdingFull": "DynamicThresholdingFull", # api 相关 "easy stableDiffusion3API": "Stable Diffusion 3 (API)", -} + # utils + "easy ckptNames": "Ckpt Names", + "easy controlnetNames": "ControlNet Names", +} \ No newline at end of file diff --git a/py/image.py b/py/image.py index 1cc22c7..e5ad8e1 100644 --- a/py/image.py +++ b/py/image.py @@ -15,6 +15,25 @@ from .libs.chooser import ChooserMessage, ChooserCancelled from .config import REMBG_DIR, REMBG_MODELS, HUMANPARSING_MODELS, MEDIAPIPE_MODELS, MEDIAPIPE_DIR +# 图像数量 +class imageCount: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + } + } + + CATEGORY = "EasyUse/Image" + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("count",) + FUNCTION = "get_count" + + def get_count(self, images): + return (images.size(0),) + # 图像裁切 class imageInsetCrop: @@ -519,6 +538,24 @@ class imageSplitList: new_images[1].append(img) return new_images +class imagesSplitImage: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + } + } + + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE") + RETURN_NAMES = ("image1", "image2", "image3", "image4", "image5") + FUNCTION = "split" + CATEGORY = "EasyUse/Image" + + def split(self, images,): + new_images = torch.chunk(images, len(images), dim=0) + return new_images + # 图片背景移除 from .briaai.rembg import BriaRMBG, preprocess_image, postprocess_image from .libs.utils import get_local_filepath, easySave, install_package @@ -901,7 +938,7 @@ class loadImageBase64: RETURN_TYPES = ("IMAGE", "MASK") OUTPUT_NODE = True FUNCTION = "load_image" - CATEGORY = "image" + CATEGORY = "EasyUse/Image/LoadImage" def convert_color(self, image,): if len(image.shape) > 2 and image.shape[2] >= 4: @@ -1005,6 +1042,7 @@ class poseEditor: NODE_CLASS_MAPPINGS = { "easy imageInsetCrop": imageInsetCrop, + "easy imageCount": imageCount, "easy imageSize": imageSize, "easy imageSizeBySide": imageSizeBySide, "easy imageSizeByLongerSide": imageSizeByLongerSide, @@ -1015,6 +1053,7 @@ NODE_CLASS_MAPPINGS = { "easy imageRatio": imageRatio, "easy imageToMask": imageToMask, "easy imageSplitList": imageSplitList, + "easy imagesSplitImage": imagesSplitImage, "easy imageSave": imageSaveSimple, "easy imageRemBg": imageRemBg, "easy imageChooser": imageChooser, @@ -1029,6 +1068,7 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "easy imageInsetCrop": "ImageInsetCrop", + "easy imageCount": "ImageCount", "easy imageSize": "ImageSize", "easy imageSizeBySide": "ImageSize (Side)", "easy imageSizeByLongerSide": "ImageSize (LongerSide)", @@ -1040,13 +1080,14 @@ NODE_DISPLAY_NAME_MAPPINGS = { "easy imageToMask": "ImageToMask", "easy imageHSVMask": "ImageHSVMask", "easy imageSplitList": "imageSplitList", + "easy imagesSplitImage": "imagesSplitImage", "easy imageSave": "SaveImage (Simple)", "easy imageRemBg": "Image Remove Bg", "easy imageChooser": "Image Chooser", "easy imageColorMatch": "Image Color Match", "easy imageInterrogator": "Image To Prompt", "easy joinImageBatch": "JoinImageBatch", - "easy loadImageBase64": "LoadImage (Base64)", + "easy loadImageBase64": "Load Image (Base64)", "easy imageToBase64": "Image To Base64", "easy humanSegmentation": "Human Segmentation", "easy poseEditor": "PoseEditor", diff --git a/py/libs/image.py b/py/libs/image.py index b8bca2a..a08a6c8 100644 --- a/py/libs/image.py +++ b/py/libs/image.py @@ -111,27 +111,26 @@ class CI_Inference: def image_to_prompt(self, image, mode, model_name='ViT-L-14/openai', low_vram=False): try: - install_package("clip_interrogator", "0.6.0") from clip_interrogator import Config, Interrogator global Config, Interrogator + except: + install_package("clip_interrogator", "0.6.0") + from clip_interrogator import Config, Interrogator - pbar = comfy.utils.ProgressBar(len(image)) + pbar = comfy.utils.ProgressBar(len(image)) - self._load_model(model_name, low_vram) - prompt = [] - for i in range(len(image)): - im = image[i] + self._load_model(model_name, low_vram) + prompt = [] + for i in range(len(image)): + im = image[i] - im = tensor2pil(im) - im = im.convert('RGB') + im = tensor2pil(im) + im = im.convert('RGB') - _prompt = self._interrogate(im, mode) - pbar.update(1) - prompt.append(_prompt) + _prompt = self._interrogate(im, mode) + pbar.update(1) + prompt.append(_prompt) - return prompt - except Exception as e: - print(e) - return [""] + return prompt ci = CI_Inference() diff --git a/py/libs/styleAlign.py b/py/libs/styleAlign.py new file mode 100644 index 0000000..fefae60 --- /dev/null +++ b/py/libs/styleAlign.py @@ -0,0 +1,148 @@ +import torch +import torch.nn as nn +from comfy.model_patcher import ModelPatcher +from typing import Union + +T = torch.Tensor + + +def exists(val): + return val is not None + + +def default(val, d): + if exists(val): + return val + return d + + +class StyleAlignedArgs: + def __init__(self, share_attn: str) -> None: + self.adain_keys = "k" in share_attn + self.adain_values = "v" in share_attn + self.adain_queries = "q" in share_attn + + share_attention: bool = True + adain_queries: bool = True + adain_keys: bool = True + adain_values: bool = True + + +def expand_first( + feat: T, + scale=1.0, +) -> T: + """ + Expand the first element so it has the same shape as the rest of the batch. + """ + b = feat.shape[0] + feat_style = torch.stack((feat[0], feat[b // 2])).unsqueeze(1) + if scale == 1: + feat_style = feat_style.expand(2, b // 2, *feat.shape[1:]) + else: + feat_style = feat_style.repeat(1, b // 2, 1, 1, 1) + feat_style = torch.cat([feat_style[:, :1], scale * feat_style[:, 1:]], dim=1) + return feat_style.reshape(*feat.shape) + + +def concat_first(feat: T, dim=2, scale=1.0) -> T: + """ + concat the the feature and the style feature expanded above + """ + feat_style = expand_first(feat, scale=scale) + return torch.cat((feat, feat_style), dim=dim) + + +def calc_mean_std(feat, eps: float = 1e-5) -> "tuple[T, T]": + feat_std = (feat.var(dim=-2, keepdims=True) + eps).sqrt() + feat_mean = feat.mean(dim=-2, keepdims=True) + return feat_mean, feat_std + +def adain(feat: T) -> T: + feat_mean, feat_std = calc_mean_std(feat) + feat_style_mean = expand_first(feat_mean) + feat_style_std = expand_first(feat_std) + feat = (feat - feat_mean) / feat_std + feat = feat * feat_style_std + feat_style_mean + return feat + +class SharedAttentionProcessor: + def __init__(self, args: StyleAlignedArgs, scale: float): + self.args = args + self.scale = scale + + def __call__(self, q, k, v, extra_options): + if self.args.adain_queries: + q = adain(q) + if self.args.adain_keys: + k = adain(k) + if self.args.adain_values: + v = adain(v) + if self.args.share_attention: + k = concat_first(k, -2, scale=self.scale) + v = concat_first(v, -2) + + return q, k, v + + +def get_norm_layers( + layer: nn.Module, + norm_layers_: "dict[str, list[Union[nn.GroupNorm, nn.LayerNorm]]]", + share_layer_norm: bool, + share_group_norm: bool, +): + if isinstance(layer, nn.LayerNorm) and share_layer_norm: + norm_layers_["layer"].append(layer) + if isinstance(layer, nn.GroupNorm) and share_group_norm: + norm_layers_["group"].append(layer) + else: + for child_layer in layer.children(): + get_norm_layers( + child_layer, norm_layers_, share_layer_norm, share_group_norm + ) + + +def register_norm_forward( + norm_layer: Union[nn.GroupNorm, nn.LayerNorm], +) -> Union[nn.GroupNorm, nn.LayerNorm]: + if not hasattr(norm_layer, "orig_forward"): + setattr(norm_layer, "orig_forward", norm_layer.forward) + orig_forward = norm_layer.orig_forward + + def forward_(hidden_states: T) -> T: + n = hidden_states.shape[-2] + hidden_states = concat_first(hidden_states, dim=-2) + hidden_states = orig_forward(hidden_states) # type: ignore + return hidden_states[..., :n, :] + + norm_layer.forward = forward_ # type: ignore + return norm_layer + + +def register_shared_norm( + model: ModelPatcher, + share_group_norm: bool = True, + share_layer_norm: bool = True, +): + norm_layers = {"group": [], "layer": []} + get_norm_layers(model.model, norm_layers, share_layer_norm, share_group_norm) + print( + f"Patching {len(norm_layers['group'])} group norms, {len(norm_layers['layer'])} layer norms." + ) + return [register_norm_forward(layer) for layer in norm_layers["group"]] + [ + register_norm_forward(layer) for layer in norm_layers["layer"] + ] + + +SHARE_NORM_OPTIONS = ["both", "group", "layer", "disabled"] +SHARE_ATTN_OPTIONS = ["q+k", "q+k+v", "disabled"] + + +def styleAlignBatch(model, share_norm, share_attn, scale=1.0): + m = model.clone() + share_group_norm = share_norm in ["group", "both"] + share_layer_norm = share_norm in ["layer", "both"] + register_shared_norm(model, share_group_norm, share_layer_norm) + args = StyleAlignedArgs(share_attn) + m.set_model_attn1_patch(SharedAttentionProcessor(args, scale)) + return m \ No newline at end of file diff --git a/py/libs/utils.py b/py/libs/utils.py index 235b6ce..11f4fa5 100644 --- a/py/libs/utils.py +++ b/py/libs/utils.py @@ -98,12 +98,17 @@ import comfy.supported_models_base def get_sd_version(model): base: BaseModel = model.model model_config: comfy.supported_models.supported_models_base.BASE = base.model_config + print(model_config) if isinstance(model_config, comfy.supported_models.SDXL): return 'sdxl' elif isinstance( model_config, (comfy.supported_models.SD15, comfy.supported_models.SD20) ): return 'sd15' + elif isinstance( + model_config, (comfy.supported_models.SVD_img2vid) + ): + return 'svd' else: return 'unknown' @@ -231,7 +236,7 @@ def easySave(images, filename_prefix, output_type, prompt=None, extra_pnginfo=No from nodes import PreviewImage, SaveImage if output_type == "Hide": return list() - if output_type == "Preview": + if output_type in ["Preview", "Preview&Choose"]: filename_prefix = 'easyPreview' results = PreviewImage().save_images(images, filename_prefix, prompt, extra_pnginfo) return results['ui']['images'] diff --git a/py/logic.py b/py/logic.py index 8e3663c..c674532 100644 --- a/py/logic.py +++ b/py/logic.py @@ -277,6 +277,30 @@ class imageSwitch: else: return (image_b, ) +class textSwitch: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input": ("INT", {"default": 1, "min": 1, "max": 2}), + }, + "optional": { + "text1": ("STRING", {"forceInput": True}), + "text2": ("STRING", {"forceInput": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("STRING",) + CATEGORY = "EasyUse/Logic/Switch" + FUNCTION = "switch" + + def switch(self, input, text1=None, text2=None,): + if input == 1: + return (text1,) + else: + return (text2,) + # ---------------------------------------------------------------运算 开始----------------------------------------------------------------------# COMPARE_FUNCTIONS = { @@ -549,6 +573,9 @@ class clearCacheAll: remove_cache('*') return () + + + NODE_CLASS_MAPPINGS = { "easy string": String, "easy int": Int, @@ -558,6 +585,7 @@ NODE_CLASS_MAPPINGS = { "easy boolean": Boolean, "easy compare": Compare, "easy imageSwitch": imageSwitch, + "easy textSwitch": textSwitch, "easy if": If, "easy isSDXL": isSDXL, "easy xyAny": xyAny, @@ -577,6 +605,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "easy boolean": "Boolean", "easy compare": "Compare", "easy imageSwitch": "Image Switch", + "easy textSwitch": "Text Switch", "easy if": "If", "easy isSDXL": "Is SDXL", "easy xyAny": "XYAny", diff --git a/web/css/chooser.css b/web/css/chooser.css new file mode 100644 index 0000000..3841061 --- /dev/null +++ b/web/css/chooser.css @@ -0,0 +1,30 @@ +.easyuse-chooser-dialog{ + max-width: 600px; +} +.easyuse-chooser-dialog-title{ + font-size: 18px; + font-weight: 700; + text-align: center; + color:var(--input-text); + margin:0; +} +.easyuse-chooser-dialog-images{ + margin-top:10px; + display: flex; + flex-wrap: wrap; + width: 100%; + box-sizing: border-box; +} +.easyuse-chooser-dialog-images img{ + width: 50%; + height: auto; + cursor: pointer; + box-sizing: border-box; + filter:brightness(80%); +} +.easyuse-chooser-dialog-images img:hover{ + filter:brightness(100%); +} +.easyuse-chooser-dialog-images img.selected{ + border: 4px solid var(--success-color); +} \ No newline at end of file diff --git a/web/css/index.css b/web/css/index.css index 8284a4f..21f103b 100644 --- a/web/css/index.css +++ b/web/css/index.css @@ -5,4 +5,5 @@ @import "contextmenu.css"; @import "modelinfo.css"; @import "toast.css"; -@import "account.css"; \ No newline at end of file +@import "account.css"; +@import "chooser.css"; \ No newline at end of file diff --git a/web/js/common/i18n.js b/web/js/common/i18n.js index de1deda..53711f3 100644 --- a/web/js/common/i18n.js +++ b/web/js/common/i18n.js @@ -59,6 +59,9 @@ const zhCN = { "APIKEY is not Empty": "APIKEY 不能为空", "Add Account": "添加账号", "Getting Your APIKEY": "获取您的APIKEY", + // choosers + "Choose Selected Images": "选择选中的图片", + "Choose images to continue": "选择图片以继续", // seg "Background": "背景", "Hat": "帽子", diff --git a/web/js/easy/easyDynamicWidgets.js b/web/js/easy/easyDynamicWidgets.js index 12a236f..a82628a 100644 --- a/web/js/easy/easyDynamicWidgets.js +++ b/web/js/easy/easyDynamicWidgets.js @@ -56,7 +56,7 @@ function widgetLogic(node, widget) { }else { toggleWidget(node, findWidgetByName(node, 'link_id')) } - if (widget.value === 'Hide' || widget.value === 'Preview' || widget.value == 'PreviewChooser' || widget.value === 'Sender') { + if (widget.value === 'Hide' || widget.value === 'Preview' || widget.value == 'Preview&Choose' || widget.value === 'Sender') { toggleWidget(node, findWidgetByName(node, 'save_prefix')) toggleWidget(node, findWidgetByName(node, 'output_path')) toggleWidget(node, findWidgetByName(node, 'embed_workflow')) diff --git a/web/js/easy/easyExtraMenu.js b/web/js/easy/easyExtraMenu.js index a74611d..510b475 100644 --- a/web/js/easy/easyExtraMenu.js +++ b/web/js/easy/easyExtraMenu.js @@ -273,11 +273,11 @@ const addMenu = (content, type, nodes_include, nodeType, has_submenu=true) => { has_submenu: has_submenu, callback: (value, options, e, menu, node) => showSwapMenu(value, options, e, menu, node, type, nodes_include) }) - if(type == 'loaders'){ + if(type == 'loaders') { options.unshift({ content: $t("💎 View Lora Info..."), callback: (value, options, e, menu, node) => { - const widget = node.widgets.find(cate=> cate.name == 'lora_name') + const widget = node.widgets.find(cate => cate.name == 'lora_name') let name = widget.value; if (!name || name == 'None') return new LoraInfoDialog(name).show('loras', name); @@ -522,7 +522,19 @@ app.registerExtension({ } } }) + // ckptNames + if(nodeData.name == 'easy ckptNames'){ + options.unshift({ + content: $t("💎 View Checkpoint Info..."), + callback: (value, options, e, menu, node) => { + let name = node.widgets[0].value; + if (!name || name == 'None') return + new CheckpointInfoDialog(name).show('checkpoints', name); + } + }) + } }) + // Swap提示词 if (positive_prompt.includes(nodeData.name)) { addMenu("↪️ Swap EasyPrompt", 'positive_prompt', positive_prompt, nodeType) diff --git a/web/js/easy/easyInterface.js b/web/js/easy/easyInterface.js index 591006a..6d96de6 100644 --- a/web/js/easy/easyInterface.js +++ b/web/js/easy/easyInterface.js @@ -747,6 +747,7 @@ const NODE_COLORS = { "easy promptReplace":"cyan", "easy XYInputs: Seeds++ Batch": customXYLink, "easy XYInputs: ModelMergeBlocks": customXYLink, + 'easy textSwitch': "pale_blue" } function setNodeColors(node, theme) { diff --git a/web/js/image_chooser/chooser.js b/web/js/image_chooser/chooser.js index fa53e98..2954baf 100644 --- a/web/js/image_chooser/chooser.js +++ b/web/js/image_chooser/chooser.js @@ -1,14 +1,70 @@ import { app } from "/scripts/app.js"; import { api } from "/scripts/api.js"; +import { ComfyDialog, $el } from "/scripts/ui.js"; import { restart_from_here } from "./prompt.js"; import { FlowState } from "./state.js"; import { send_cancel, send_message, send_onstart, skip_next_restart_message } from "./messaging.js"; import { display_preview_images, additionalDrawBackground, click_is_in_image } from "./preview.js"; -import {toggleWidget} from "../common/utils.js"; +import {$t} from "../common/i18n.js"; -function progressButtonPressed() { - const node = app.graph._nodes_by_id[this.node_id]; + +class chooserImageDialog extends ComfyDialog { + + constructor() { + super(); + this.node = null + this.select_index = [] + this.dialog_div = null + } + + show(image,node){ + this.select_index = [] + this.node = node + + const images_div = image.map((img, index) => { + const imgEl = $el('img', { + src: img.src, + onclick: _ => { + if(this.select_index.includes(index)){ + this.select_index = this.select_index.filter(i => i !== index) + imgEl.classList.remove('selected') + } else { + this.select_index.push(index) + imgEl.classList.add('selected') + } + if (node.selected.has(index)) node.selected.delete(index); + else node.selected.add(index); + } + }) + return imgEl + }) + super.show($el('div.easyuse-chooser-dialog',[ + $el('h5.easyuse-chooser-dialog-title', $t('Choose images to continue')), + $el('div.easyuse-chooser-dialog-images',images_div) + ])) + } + createButtons() { + const btns = super.createButtons(); + btns[0].onclick = _ => { + cancelButtonPressed() + super.close() + } + btns.unshift($el('button', { + type: 'button', + textContent: $t('Choose Selected Images'), + onclick: _ => { + progressButtonPressed(this.node) + super.close() + } + })) + return btns + } + +} + +function progressButtonPressed(_node) { + const node = _node ? _node : app.graph._nodes_by_id[this.node_id]; if (node) { if (FlowState.paused()) { send_message(node.id, [...node.selected, -1, ...node.anti_selected]); @@ -29,7 +85,11 @@ app.registerExtension({ }, setup(app) { function easyuseImageChooser(event) { - display_preview_images(event); + const {node,image,isKSampler} = display_preview_images(event); + if(isKSampler) { + const dialog = new chooserImageDialog(); + dialog.show(image,node) + } } api.addEventListener("easyuse-image-choose", easyuseImageChooser); diff --git a/web/js/image_chooser/preview.js b/web/js/image_chooser/preview.js index 2bb6a89..588849f 100644 --- a/web/js/image_chooser/preview.js +++ b/web/js/image_chooser/preview.js @@ -1,11 +1,14 @@ import { app } from "/scripts/app.js"; +const kSampler = ['easy kSampler', 'easy kSamplerTiled', 'easy fullkSampler'] + function display_preview_images(event) { const node = app.graph._nodes_by_id[event.detail.id]; if (node) { node.selected = new Set(); node.anti_selected = new Set(); - showImages(node, event.detail.urls); + const image = showImages(node, event.detail.urls); + return {node,image,isKSampler:kSampler.includes(node.type)} } else { console.log(`Image Chooser Preview - failed to find ${event.detail.id}`) } @@ -20,6 +23,7 @@ function showImages(node, urls) { img.src = `/view?filename=${encodeURIComponent(u.filename)}&type=temp&subfolder=${app.getPreviewFormatParam()}` }) node.setSizeForImage?.(); + return node.imgs } function drawRect(node, s, ctx) {