diff --git a/ComfyUI-Easy-Use.json b/ComfyUI-Easy-Use.json index cd90397..a1bee37 100644 --- a/ComfyUI-Easy-Use.json +++ b/ComfyUI-Easy-Use.json @@ -14,6 +14,9 @@ "easy controlnetLoader": { "title": "简易Controlnet" }, + "easy controlnetLoaderADV": { + "title": "简易Controlnet(高级)" + }, "easy LLLite": { "title": "简易LLLite" }, diff --git a/README.en.md b/README.en.md index e8f2fe2..2b152cb 100644 --- a/README.en.md +++ b/README.en.md @@ -19,15 +19,26 @@ EasyUse is simplified on the basis of [tinyterraNodes](https://github.com/TinyTe ### Updated -**[Updated at 12/13/2023]** +**2023-12-14** + +- `easy a1111Loader` and `easy comfyLoader` added `batch_size` of required input parameters +- Added the `easy controlnetLoaderADV` node +- `easy controlnetLoaderADV` and `easy controlnetLoader` added `control_net ` of optional input parameters +- `easy preSampling` and `easy preSamplingAdvanced` added 'image_to_latent' optional input parameters +- Added the `easy imageSizeBySide` node, which can be output as a long side or a short side + +
+Updated at 12/13/2023 - Added the `easy LLLiteLoader` node, if you have pre-installed the kohya-ss/ControlNet-LLLite-ComfyUI package, please move the model files in the models to `ComfyUI\models\controlnet\` (i.e. in the default controlnet path of comfy, please do not change the file name of the model, otherwise it will not be read). - Modify `easy controlnetLoader` to the bottom of the loader category. - Added size display for `easy imageSize` and `easy imageSizeByLongerSize` outputs. +
-**[Updated at 12/11/2023]** - +
+Updated at 12/11/2023 - Added the `showSpentTime` node to display the time spent on image diffusion and the time spent on VAE decoding images +
### Major optimizations diff --git a/README.md b/README.md index 43f73f7..84b38b8 100644 --- a/README.md +++ b/README.md @@ -15,19 +15,31 @@ -EasyUse 在 [tinyterraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes) 的基础上做了简化,在简化的节点中去除了过多的传入和传出参数,建议您配合 [tinyterraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes) 中的 **pipeIn**、**pipeOut**、**pipeEdit** 使用,可参考下方示例里 [图生图的工作流](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#%E5%9B%BE%E7%94%9F%E5%9B%BEcontrolnet)。 +EasyUse 在 [tinyterraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes) 的基础上做了简化,在简化的节点中去除了过多的传入和传出参数,建议您配合 [tinyterraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes) 中的 **pipeIn**、**pipeOut**、**pipeEdit** 使用,可参考下方示例里 [工作流](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#sdturbo%E9%AB%98%E6%B8%85%E4%BF%AE%E5%A4%8Dsvd)。 ### 更新 -**2023-12-13** +**2023-12-14** + +- `easy a1111Loader` 和 `easy comfyLoader` 新增 `batch_size` 传入参数 +- 新增 `easy controlnetLoaderADV` 节点 +- `easy controlnetLoaderADV` 和 `easy controlnetLoader` 新增 `control_net` 可选传入参数 +- `easy preSampling` 和 `easy preSamplingAdvanced` 新增 `image_to_latent` 可选传入参数 +- 新增 `easy imageSizeBySide` 节点,可选输出为长边或短边 + +
+2023-12-13 - 新增 `easy LLLiteLoader` 节点,如果您预先安装过 kohya-ss/ControlNet-LLLite-ComfyUI 包,请将 models 里的模型文件移动至 ComfyUI\models\controlnet\ (即comfy默认的controlnet路径里,请勿修改模型的文件名,不然会读取不到)。 - 修改 `easy controlnetLoader` 到 loader 分类底下。 - 新增 `easy imageSize` 和 `easy imageSizeByLongerSize` 输出的尺寸显示。 +
-**2023-12-11** +
+2023-12-11 - 新增 `easy showSpentTime` 节点用于展示图片推理花费时间与VAE解码花费时间。 +
### 主要的优化 diff --git a/docs/image_to_image_controlnet.png b/docs/image_to_image_controlnet.png index f857695..cc91771 100644 Binary files a/docs/image_to_image_controlnet.png and b/docs/image_to_image_controlnet.png differ diff --git a/docs/text_to_image.png b/docs/text_to_image.png index eb791aa..33fbec6 100644 Binary files a/docs/text_to_image.png and b/docs/text_to_image.png differ diff --git a/py/easyNodes.py b/py/easyNodes.py index 522bae2..9e8ae67 100644 --- a/py/easyNodes.py +++ b/py/easyNodes.py @@ -28,7 +28,7 @@ from comfy_extras.chainner_models import model_loading from typing import Dict, List, Optional, Tuple, Union, Any from .adv_encode import advanced_encode, advanced_encode_XL -from nodes import MAX_RESOLUTION, VAEEncode, VAEEncodeTiled, VAEDecode, VAEDecodeTiled +from nodes import MAX_RESOLUTION, RepeatLatentBatch from .config import BASE_RESOLUTIONS from server import PromptServer @@ -781,6 +781,7 @@ class a1111Loader: "positive": ("STRING", {"default": "Positive", "multiline": True}), "negative": ("STRING", {"default": "Negative", "multiline": True}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), }, "optional": {"optional_lora_stack": ("LORA_STACK",)}, "hidden": {"prompt": "PROMPT", "positive_weight_interpretation": "A1111", "negative_weight_interpretation": "A1111"}, "my_unique_id": "UNIQUE_ID"} @@ -794,7 +795,7 @@ class a1111Loader: def adv_pipeloader(self, ckpt_name, vae_name, clip_skip, lora_name, lora_model_strength, lora_clip_strength, resolution, empty_latent_width, empty_latent_height, - positive, negative, optional_lora_stack=None, prompt=None, + positive, negative, batch_size, optional_lora_stack=None, prompt=None, positive_weight_interpretation='A1111', negative_weight_interpretation='A1111', my_unique_id=None ): @@ -813,7 +814,7 @@ class a1111Loader: raise ValueError("Invalid base_resolution format.") # Create Empty Latent - latent = torch.zeros([1, 4, empty_latent_height // 8, empty_latent_width // 8]).cpu() + latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8]).cpu() samples = {"samples": latent} # Clean models from loaded_objects @@ -894,7 +895,7 @@ class a1111Loader: "negative_balance": None, "empty_latent_width": empty_latent_width, "empty_latent_height": empty_latent_height, - "batch_size": 1, + "batch_size": batch_size, "seed": 0, "empty_samples": samples, } } @@ -921,6 +922,8 @@ class comfyLoader: "positive": ("STRING", {"default": "Positive", "multiline": True}), "negative": ("STRING", {"default": "Negative", "multiline": True}), + + "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), }, "optional": {"optional_lora_stack": ("LORA_STACK",)}, "hidden": {"prompt": "PROMPT", "positive_weight_interpretation": "comfy", "negative_weight_interpretation": "comfy"}, "my_unique_id": "UNIQUE_ID"} @@ -934,7 +937,7 @@ class comfyLoader: def adv_pipeloader(self, ckpt_name, vae_name, clip_skip, lora_name, lora_model_strength, lora_clip_strength, resolution, empty_latent_width, empty_latent_height, - positive, negative, optional_lora_stack=None, prompt=None, + positive, negative, batch_size, optional_lora_stack=None, prompt=None, positive_weight_interpretation='comfy', negative_weight_interpretation='comfy', my_unique_id=None ): @@ -943,7 +946,7 @@ class comfyLoader: ckpt_name, vae_name, clip_skip, lora_name, lora_model_strength, lora_clip_strength, resolution, empty_latent_width, empty_latent_height, - positive, negative, optional_lora_stack, prompt, + positive, negative, batch_size, optional_lora_stack, prompt, positive_weight_interpretation, negative_weight_interpretation, my_unique_id ) @@ -958,12 +961,11 @@ class controlnetSimple: return { "required": { "pipe": ("PIPE_LINE",), - "control_net_name": (folder_paths.get_filename_list("controlnet"),), "image": ("IMAGE",), + "control_net_name": (folder_paths.get_filename_list("controlnet"),), }, "optional": { - "positive": ("CONDITIONING",), - "negative": ("CONDITIONING",), + "control_net": ("CONTROL_NET",), "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}) } } @@ -975,16 +977,17 @@ class controlnetSimple: FUNCTION = "controlnetApply" CATEGORY = "EasyUse/Loader" - def controlnetApply(self, pipe, control_net_name, image, positive=None, negative=None, strength=1): - controlnet_path = folder_paths.get_full_path("controlnet", control_net_name) - control_net = comfy.controlnet.load_controlnet(controlnet_path) + def controlnetApply(self, pipe, image, control_net_name, control_net=None,strength=1): + if control_net is None: + controlnet_path = folder_paths.get_full_path("controlnet", control_net_name) + control_net = comfy.controlnet.load_controlnet(controlnet_path) control_hint = image.movedim(-1, 1) - _positive = pipe["positive"] if positive is None else positive - _negative = pipe["negative"] if negative is None else negative + positive = pipe["positive"] + negative = pipe["negative"] if strength != 0: - if _negative is None: + if negative is None: p = [] for t in positive: n = [t[0], t[1].copy()] @@ -994,11 +997,11 @@ class controlnetSimple: n[1]['control'] = c_net n[1]['control_apply_to_uncond'] = True p.append(n) - _positive = p + positive = p else: cnets = {} out = [] - for conditioning in [_positive, _negative]: + for conditioning in [positive, negative]: c = [] for t in conditioning: d = t[1].copy() @@ -1016,12 +1019,94 @@ class controlnetSimple: n = [t[0], d] c.append(n) out.append(c) - _positive = out[0] - _negative = out[1] + positive = out[0] + negative = out[1] - # 拼接条件 - positive = _positive if positive is None else _positive + pipe['positive'] - negative = _negative if negative is None else _negative + pipe['negative'] + new_pipe = { + "model": pipe['model'], + "positive": positive, + "negative": negative, + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": pipe["samples"], + "images": pipe["images"], + "seed": 0, + + "loader_settings": pipe["loader_settings"] + } + + return (new_pipe,) + +# controlnetADV +class controlnetAdvanced: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "control_net_name": (folder_paths.get_filename_list("controlnet"),), + }, + "optional": { + "control_net": ("CONTROL_NET",), + "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}) + } + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + OUTPUT_NODE = True + + FUNCTION = "controlnetApply" + CATEGORY = "EasyUse/Loader" + + def controlnetApply(self, pipe, image, control_net_name, control_net=None, strength=1, start_percent=0, end_percent=1): + if control_net is None: + controlnet_path = folder_paths.get_full_path("controlnet", control_net_name) + control_net = comfy.controlnet.load_controlnet(controlnet_path) + control_hint = image.movedim(-1, 1) + + positive = pipe["positive"] + negative = pipe["negative"] + + if strength != 0: + if negative is None: + p = [] + for t in positive: + n = [t[0], t[1].copy()] + c_net = control_net.copy().set_cond_hint(control_hint, strength) + if 'control' in t[1]: + c_net.set_previous_controlnet(t[1]['control']) + n[1]['control'] = c_net + n[1]['control_apply_to_uncond'] = True + p.append(n) + positive = p + else: + cnets = {} + out = [] + for conditioning in [positive, negative]: + c = [] + for t in conditioning: + d = t[1].copy() + + prev_cnet = d.get('control', None) + if prev_cnet in cnets: + c_net = cnets[prev_cnet] + else: + c_net = control_net.copy().set_cond_hint(control_hint, strength, (start_percent, end_percent)) + c_net.set_previous_controlnet(prev_cnet) + cnets[prev_cnet] = c_net + + d['control'] = c_net + d['control_apply_to_uncond'] = False + n = [t[0], d] + c.append(n) + out.append(c) + positive = out[0] + negative = out[1] new_pipe = { "model": pipe['model'], @@ -1095,6 +1180,9 @@ class samplerSettings: "seed_num": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}), "control_before_generate": (["fixed", "increment", "decrement", "randomize"], {"default": "randomize"}), }, + "optional": { + "image_to_latent": ("IMAGE",), + }, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, } @@ -1106,7 +1194,7 @@ class samplerSettings: FUNCTION = "settings" CATEGORY = "EasyUse/PreSampling" - def settings(self, pipe, steps, cfg, sampler_name, scheduler, denoise, seed_num, control_before_generate, prompt=None, extra_pnginfo=None, my_unique_id=None): + def settings(self, pipe, steps, cfg, sampler_name, scheduler, denoise, seed_num, control_before_generate, image_to_latent=None, prompt=None, extra_pnginfo=None, my_unique_id=None): # seed生成 seed_num = control_seed(control_before_generate, seed_num) @@ -1117,6 +1205,17 @@ class samplerSettings: length = len(node["widgets_values"]) node["widgets_values"][length-2] = seed_num + vae = pipe["vae"] + # 图生图转换 + if image_to_latent is not None: + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + samples = {"samples": vae.encode(image_to_latent)} + samples = RepeatLatentBatch().repeat(samples, batch_size)[0] + images = image_to_latent + else: + samples = pipe["samples"] + images = pipe["images"] + print(samples) new_pipe = { "model": pipe['model'], "positive": pipe['positive'], @@ -1124,8 +1223,8 @@ class samplerSettings: "vae": pipe['vae'], "clip": pipe['clip'], - "samples": pipe["samples"], - "images": pipe["images"], + "samples": samples, + "images": images, "seed": seed_num, "loader_settings": { @@ -1163,6 +1262,9 @@ class samplerSettingsAdvanced: "seed_num": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}), "control_before_generate": (["fixed", "increment", "decrement", "randomize"], {"default": "randomize"}), }, + "optional": { + "image_to_latent": ("Image",) + }, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, } @@ -1174,7 +1276,7 @@ class samplerSettingsAdvanced: FUNCTION = "settings" CATEGORY = "EasyUse/PreSampling" - def settings(self, pipe, steps, cfg, sampler_name, scheduler, start_at_step, end_at_step, add_noise, seed_num, control_before_generate, prompt=None, extra_pnginfo=None, my_unique_id=None): + def settings(self, pipe, steps, cfg, sampler_name, scheduler, start_at_step, end_at_step, add_noise, seed_num, control_before_generate, image_to_latent=None, prompt=None, extra_pnginfo=None, my_unique_id=None): # seed生成 seed_num = control_seed(control_before_generate, seed_num) @@ -1185,6 +1287,17 @@ class samplerSettingsAdvanced: length = len(node["widgets_values"]) node["widgets_values"][length-2] = seed_num + # 图生图转换 + vae = pipe["vae"] + if image_to_latent is not None: + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + samples = {"samples": vae.encode(image_to_latent)} + samples = RepeatLatentBatch().repeat(samples, batch_size)[0] + images = image_to_latent + else: + samples = pipe["samples"] + images = pipe["images"] + new_pipe = { "model": pipe['model'], "positive": pipe['positive'], @@ -1192,8 +1305,8 @@ class samplerSettingsAdvanced: "vae": pipe['vae'], "clip": pipe['clip'], - "samples": pipe["samples"], - "images": pipe["images"], + "samples": samples, + "images": images, "seed": seed_num, "loader_settings": { @@ -1516,9 +1629,6 @@ class samplerSimple: if add_noise == "disable": disable_noise = True - def vae_decode_latent(vae, samples, tile_size): - return VAEDecodeTiled().decode(vae, samples, tile_size)[0] if tile_size is not None else VAEDecode().decode(vae, samples)[0] - def process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, steps, start_step, last_step, cfg, sampler_name, scheduler, denoise, @@ -1533,7 +1643,13 @@ class samplerSimple: # 推理结束时间 end_time = int(time.time() * 1000) # 解码图片 - samp_images = vae_decode_latent(samp_vae, samp_samples, tile_size) + latent = samp_samples["samples"] + + # 解码图片 + if tile_size is not None: + samp_images = samp_vae.decode_tiled(latent, tile_x=tile_size // 8, tile_y=tile_size // 8, ) + else: + samp_images = samp_vae.decode(latent).cpu() # 推理总耗时(包含解码) end_decode_time = int(time.time() * 1000) @@ -1686,8 +1802,8 @@ class samplerSDTurbo: latent = samp_samples['samples'] # 解码图片 - if tile_size: - samp_images = (samp_vae.decode_tiled(latent, tile_x=tile_size // 8, tile_y=tile_size // 8, ),) + if tile_size is not None: + samp_images = samp_vae.decode_tiled(latent, tile_x=tile_size // 8, tile_y=tile_size // 8, ) else: samp_images = samp_vae.decode(latent).cpu() @@ -1770,6 +1886,7 @@ NODE_CLASS_MAPPINGS = { "easy a1111Loader": a1111Loader, "easy comfyLoader": comfyLoader, "easy controlnetLoader": controlnetSimple, + "easy controlnetLoaderADV": controlnetAdvanced, "easy globalSeed": globalSeed, "easy preSampling": samplerSettings, "easy preSamplingAdvanced": samplerSettingsAdvanced, @@ -1785,6 +1902,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "easy a1111Loader": "EasyLoader (A1111)", "easy comfyLoader": "EasyLoader (comfy)", "easy controlnetLoader": "EasyControlnet", + "easy controlnetLoaderADV": "EasyControlnet (Advanced)", "easy globalSeed": "GlobalSeed", "easy preSampling": "PreSampling", "easy preSamplingAdvanced": "PreSampling (Advanced)", diff --git a/py/image.py b/py/image.py index 86b42c9..5866689 100644 --- a/py/image.py +++ b/py/image.py @@ -119,7 +119,38 @@ class imageSize: result = (0, 0) return {"ui": {"text": "Width: "+str(result[0])+" , Height: "+str(result[1])}, "result": result} -# 图像尺寸 +# 图像尺寸(最长边) +class imageSizeBySide: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "side": (["Longest", "Shortest"],) + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("resolution",) + FUNCTION = "image_side" + + CATEGORY = "EasyUse/Image" + + def image_side(self, image, side): + image = tensor2pil(image) + if image.size: + if side == "Longest": + result = (image.size[0],) if image.size[0] > image.size[1] else (image.size[1],) + elif side == 'Shortest': + result = (image.size[0],) if image.size[0] < image.size[1] else (image.size[1],) + else: + result = (0,) + return {"ui": {"text": str(result[0])}, "result": result} + +# 图像尺寸(最长边) class imageSizeByLongerSide: def __init__(self): pass @@ -152,11 +183,13 @@ class imageSizeByLongerSide: NODE_CLASS_MAPPINGS = { "easy imageInsetCrop": imageInsetCrop, "easy imageSize": imageSize, + "easy imageSizeBySide": imageSizeBySide, "easy imageSizeByLongerSide": imageSizeByLongerSide } NODE_DISPLAY_NAME_MAPPINGS = { "easy imageInsetCrop": "ImageInsetCrop", "easy imageSize": "ImageSize", + "easy imageSizeBySide": "ImageSize (Side)", "easy imageSizeByLongerSide": "ImageSize (LongerSide)" } \ No newline at end of file diff --git a/py/lllite.py b/py/lllite.py index e0694c2..1801033 100644 --- a/py/lllite.py +++ b/py/lllite.py @@ -4,8 +4,8 @@ import os import folder_paths import comfy -def get_file_list(path): - return [file for file in os.listdir(path) if file != "put_models_here.txt" and "lllite" in file] +def get_file_list(filenames): + return [file for file in filenames if file != "put_models_here.txt" and "lllite" in file] def extra_options_to_module_prefix(extra_options): @@ -252,7 +252,7 @@ class LLLiteLoader: return { "required": { "model": ("MODEL",), - "model_name": (get_file_list(folder_paths.get_folder_paths("controlnet")[0]),), + "model_name": (get_file_list(folder_paths.get_filename_list("controlnet")),), "cond_image": ("IMAGE",), "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), "steps": ("INT", {"default": 0, "min": 0, "max": 200, "step": 1}), @@ -268,7 +268,7 @@ class LLLiteLoader: def load_lllite(self, model, model_name, cond_image, strength, steps, start_percent, end_percent): # cond_image is b,h,w,3, 0-1 - model_path = os.path.join(folder_paths.get_folder_paths("controlnet")[0], model_name) + model_path = os.path.join(folder_paths.get_full_path("controlnet", model_name)) model_lllite = model.clone() patch = load_control_net_lllite_patch(model_path, cond_image, strength, steps, start_percent, end_percent) diff --git a/web/js/image.js b/web/js/image.js index 9f38cac..b753103 100644 --- a/web/js/image.js +++ b/web/js/image.js @@ -5,7 +5,7 @@ app.registerExtension({ name: "comfy.easyUse.imageWidgets", nodeCreated(node) { - if (["easy imageSize","easy imageSizeByLongerSide"].includes(node.comfyClass)) { + if (["easy imageSize","easy imageSizeBySide","easy imageSizeByLongerSide"].includes(node.comfyClass)) { const inputEl = document.createElement("textarea"); inputEl.className = "comfy-multiline-input"; @@ -29,7 +29,7 @@ app.registerExtension({ }, beforeRegisterNodeDef(nodeType, nodeData, app) { - if (["easy imageSize","easy imageSizeByLongerSide"].includes(nodeData.name)) { + if (["easy imageSize","easy imageSizeBySide","easy imageSizeByLongerSide"].includes(nodeData.name)) { function populate(arr_text) { var text = ''; for (let i = 0; i < arr_text.length; i++){