From c6edbb21b60e60b103001a2cac9fba3290341d2d Mon Sep 17 00:00:00 2001 From: Level Pixel Dev Date: Wed, 13 Nov 2024 03:35:29 +0600 Subject: [PATCH] Added Load LoRA Tag --- README.md | 20 +++++- __init__.py | 1 + nodes/io/lora_tag_loader_LP.py | 101 +++++++++++++++++++++++++++ pyproject.toml | 2 +- requirements.txt | 2 +- web/js/{autotagger.js => widgets.js} | 68 +++++++++++++++++- 6 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 nodes/io/lora_tag_loader_LP.py rename web/js/{autotagger.js => widgets.js} (66%) diff --git a/README.md b/README.md index 75a1368..c361950 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ It will attempt to use symlinks and junctions to prevent having to copy files an All nodes Level Pixel: -level-pixel-nodes +level-pixel-nodes_2 ## LLM nodes @@ -60,6 +60,14 @@ The core functionality is taken from [ComfyUI_VLM_nodes](https://github.com/goka A more improved version of rembg nodes for ComfyUI with an extended list of models. +To use on GPU, at least CUDA 12.4 (Pytorch cu124) is required, so I recommend upgrading to newer versions of ComfyUI and Pytorch. +If GPU still doesn't work, run: + +``` +pip uninstall rembg +pip install rembg[gpu] +``` + The core functionality is taken from [RemBG nodes for ComfyUI](https://github.com/Loewen-Hob/rembg-comfyui-node-better) and belongs to its authors. ## Autotagger @@ -78,6 +86,12 @@ Nodes are very convenient because you can use them to remove unnecessary tags by The core functionality is taken from [comfyui_tag_fillter](https://github.com/sugarkwork/comfyui_tag_fillter) and belongs to its authors. +## Load LoRA Tag + +LoRA loader from text in the style of Automatic1111 and Forge WebUI. For this version of loader, text output for errors when loading LoRA has been added as widget on node. + +The core functionality is taken from [comfyui_lora_tag_loader](https://github.com/badjeff/comfyui_lora_tag_loader) and belongs to its authors. + ## Model Unloader nodes A node that automatically unloads all checkpoints from memory. It must be added to a sequential chain of nodes in the workflow. There are three versions of this node: Hard (complete unloading of all checkpoints from memory, except for GGUF (not supported yet)), Middle (the same as Hard, but in the future I plan to add widgets with the ability to select a mode), Soft (without unloading checkpoints from memory, just soft cleaning of memory from garbage). @@ -121,12 +135,14 @@ VLM nodes for ComfyUI/[ComfyUI_VLM_nodes](https://github.com/gokayfem/ComfyUI_VL Tag Filter nodes for ComfyUI/[comfyui_tag_fillter](https://github.com/sugarkwork/comfyui_tag_fillter) - Best tag filter by category nodes for ComfyUI. +Load LoRA Tag node for ComfyUI/[comfyui_lora_tag_loader](https://github.com/badjeff/comfyui_lora_tag_loader) - Thanks to the author for this great node for LoRAs! + RemBG nodes for ComfyUI/[rembg-comfyui-node](https://github.com/Loewen-Hob/rembg-comfyui-node-better) - RemBG nodes for ComfyUI. RemBG software package/[rembg](https://github.com/danielgatis/rembg) - Best software to remove background for any object in the picture. # License -Copyright (c) 2024-present Level Pixel +Copyright (c) 2024-present [Level Pixel](https://github.com/LevelPixel) Licensed under Apache License diff --git a/__init__.py b/__init__.py index 8655bca..ddf57b9 100644 --- a/__init__.py +++ b/__init__.py @@ -39,6 +39,7 @@ node_list = [ "io.image_loaders_LP", "io.image_outputs_LP", "io.text_outputs_LP", + "io.lora_tag_loader_LP", "llm.llm_LP", "tags.tags_utils_LP", "text.text_utils_LP", diff --git a/nodes/io/lora_tag_loader_LP.py b/nodes/io/lora_tag_loader_LP.py new file mode 100644 index 0000000..7e4e80f --- /dev/null +++ b/nodes/io/lora_tag_loader_LP.py @@ -0,0 +1,101 @@ +from pathlib import Path +import folder_paths +import re + +# Import ComfyUI files +import comfy.sd +import comfy.utils + +class LoraTagLoader: + def __init__(self): + self.loaded_lora = None + self.tag_pattern = r"\<[0-9a-zA-Z\:\_\-\.\s\/\(\)\\\\]+\>" + + @classmethod + def INPUT_TYPES(s): + return {"required": { "model": ("MODEL",), + "clip": ("CLIP", ), + "text": ("STRING", {"multiline": True}), + }} + RETURN_TYPES = ("MODEL", "CLIP", "STRING") + RETURN_NAMES = ("MODEL", "CLIP", "STRING") + FUNCTION = "load_lora" + OUTPUT_NODE = False + + CATEGORY = "LevelPixel/IO" + + def load_lora(self, model, clip, text): + # print(f"\nLoraTagLoader input text: { text }") + + founds = re.findall(self.tag_pattern, text) + # print(f"\nfoound lora tags: { founds }") + + if len(founds) < 1: + return (model, clip, text) + + model_lora = model + clip_lora = clip + + log = [] + log.append("") + + lora_files = folder_paths.get_filename_list("loras") + for f in founds: + tag = f[1:-1] + pak = tag.split(":") + type = pak[0] + if type != 'lora': + continue + name = None + if len(pak) > 1 and len(pak[1]) > 0: + name = pak[1] + else: + continue + wModel = wClip = 0 + try: + if len(pak) > 2 and len(pak[2]) > 0: + wModel = float(pak[2]) + wClip = wModel + if len(pak) > 3 and len(pak[3]) > 0: + wClip = float(pak[3]) + except ValueError: + continue + if name == None: + continue + lora_name = None + for lora_file in lora_files: + if Path(lora_file).name.startswith(name) or lora_file.startswith(name): + lora_name = lora_file + break + if lora_name == None: + log[0] = log[0] + f"NOT found LoRA '{name}' \n" + print(f"bypassed lora tag: { (type, name, wModel, wClip) } >> { lora_name }") + continue + print(f"detected lora tag: { (type, name, wModel, wClip) } >> { lora_name }") + + lora_path = folder_paths.get_full_path("loras", lora_name) + lora = None + if self.loaded_lora is not None: + if self.loaded_lora[0] == lora_path: + lora = self.loaded_lora[1] + else: + temp = self.loaded_lora + self.loaded_lora = None + del temp + + if lora is None: + lora = comfy.utils.load_torch_file(lora_path, safe_load=True) + self.loaded_lora = (lora_path, lora) + + model_lora, clip_lora = comfy.sd.load_lora_for_models(model_lora, clip_lora, lora, wModel, wClip) + + plain_prompt = re.sub(self.tag_pattern, "", text) + return {"ui": {"log": log}, "result": (model_lora, clip_lora, plain_prompt)} + +NODE_CLASS_MAPPINGS = { + "LoraTagLoader|LP": LoraTagLoader, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LoraTagLoader|LP": "Load LoRA Tag [LP]", +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5fd9cc8..054aaa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_level_pixel" description = "Various nodes of the Level Pixel company. Includes convenient advanced nodes for working with images from folders; counting files in a folder; cleaning memory; tag filters. Model Unloader, LLM Unloader (GGUF unloaders), Free memory, Tag Filters, Tag Category Filters, Tag Choice Parser, File counter, Image Loader From Path (with counters), Image Remove Background based on RemBG, Autotagger." -version = "1.0.7" +version = "1.0.8" license = { file = "LICENSE" } dependencies = ["torch>=2.0.1", "torchvision>=0.15.2", "numpy", "matplotlib", "scikit-build-core>=0.10.7", "rembg>=2.0.59", "onnxruntime-gpu>=1.18.0", "onnxruntime>=1.20.0"] diff --git a/requirements.txt b/requirements.txt index c3c3660..9eb1cd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,5 @@ numpy matplotlib scikit-build-core>=0.10.7 rembg>=2.0.59 -onnxruntime-gpu>=1.18.0 +onnxruntime-gpu>=1.20.0 onnxruntime>=1.20.0 \ No newline at end of file diff --git a/web/js/autotagger.js b/web/js/widgets.js similarity index 66% rename from web/js/autotagger.js rename to web/js/widgets.js index fa4738c..25d5e56 100644 --- a/web/js/autotagger.js +++ b/web/js/widgets.js @@ -80,7 +80,7 @@ app.registerExtension({ async beforeRegisterNodeDef(nodeType, nodeData, app) { levelpixel.addStatusTagHandler(nodeType); - if (nodeData.name === "Autotagger|LevelPixel") { + if (nodeData.name === "Autotagger|LP") { const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (message) { const r = onExecuted?.apply?.(this, arguments); @@ -139,3 +139,69 @@ app.registerExtension({ } }, }); + + +app.registerExtension({ + name: "levelpixel.LoraTagLoader", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + levelpixel.addStatusTagHandler(nodeType); + + if (nodeData.name === "LoraTagLoader|LP") { + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + const r = onExecuted?.apply?.(this, arguments); + + const pos = this.widgets.findIndex((w) => w.name === "log"); + if (pos !== -1) { + for (let i = pos; i < this.widgets.length; i++) { + this.widgets[i].onRemove?.(); + } + this.widgets.length = pos; + } + + for (const list of message.log) { + const w = ComfyWidgets["STRING"](this, "log", ["STRING", { multiline: true }], app).widget; + w.inputEl.readOnly = true; + w.inputEl.style.opacity = 0.6; + w.value = list; + } + + this.onResize?.(this.size); + + return r; + }; + } else { + const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; + nodeType.prototype.getExtraMenuOptions = function (_, options) { + const r = getExtraMenuOptions?.apply?.(this, arguments); + let img; + if (this.imageIndex != null) { + // An image is selected so select that + img = this.imgs[this.imageIndex]; + } else if (this.overIndex != null) { + // No image is selected but one is hovered + img = this.imgs[this.overIndex]; + } + if (img) { + let pos = options.findIndex((o) => o.content === "Save Image"); + if (pos === -1) { + pos = 0; + } else { + pos++; + } + options.splice(pos, 0, { + content: "LoraTagLoader", + callback: async () => { + let src = img.src; + src = src.replace("/view?", `/levelpixel/loratagloader/tag?node=${this.id}&clientId=${api.clientId}&`); + const res = await (await fetch(src)).json(); + alert(res); + }, + }); + } + + return r; + }; + } + }, +});