Added autotagger and other new nodes

Added autotagger, allowing to generate tags for images. With the ability to use autotagger in cycles and conditions.
Added new convenient nodes.
Fixed and optimized some previous nodes.
Added more description and sources.
This commit is contained in:
Level Pixel Dev
2024-11-12 08:26:17 +06:00
parent f1f76e1237
commit 35f0c255e0
13 changed files with 958 additions and 79 deletions
+1
View File
@@ -0,0 +1 @@
__pycache__
+86 -9
View File
@@ -1,30 +1,82 @@
## Level Pixel nodes for ComfyUI
![banner_LevelPixel_with_logo](https://github.com/user-attachments/assets/ef79f2c9-04fb-485f-aba5-6cd00cb14d8c)
In this Level Pixel node pack you will find:
LLM nodes, LLaVa nodes, Image Remove Background based on RemBG, Tag Category Filter nodes, Model Unloader nodes, File Counter, Image Loader From Path, Load Image, Fast Checker Pattern, Simple Float Slider.
LLM nodes, LLaVa nodes, Image Remove Background based on RemBG, Tag Category Filter nodes, Model Unloader nodes, Autotagger, File Counter, Image Loader From Path, Load Image, Fast Checker Pattern, Simple Float Slider.
## Contacts:
For cooperation, suggestions and ideas you can write to email:
levelpixel.dev@gmail.com
# Installation:
## Installation Using ComfyUI Manager (recommended):
Install [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) and do steps introduced there to install this repo 'ComfyUI-LevelPixel'.
The nodes of the current package will be updated automatically when you click "Update ALL" in ComfyUI Manager.
## Alternative installation:
Clone the repository:
`git clone https://github.com/LevelPixel/ComfyUI-LevelPixel.git`
to your ComfyUI `custom_nodes` directory
The script will then automatically install all custom scripts and nodes.
It will attempt to use symlinks and junctions to prevent having to copy files and keep them up to date.
- For uninstallation:
- Delete the cloned repo in `custom_nodes`
- Ensure `web/extensions/levelpixel` has also been removed
- For manual update:
- Navigate to the cloned repo e.g. `custom_nodes/ComfyUI-LevelPixel`
- `git pull`
# Features
All nodes Level Pixel:
<img width="1173" alt="level-pixel-nodes" src="https://github.com/user-attachments/assets/60623f84-b02a-4749-9e2c-4ab60431b383">
## LLM nodes
A node that generates text using the LLM model with subsequent unloading of the model from memory. Useful in those workflows where there is constant switching between different models and technologies under conditions of insufficient RAM of the video processor.
Our LLM nodes support the latest LLM and CLIP models, and should support future ones (please let us know if any models stop working).
Most of the functionality is taken from this package - [https://github.com/gokayfem/ComfyUI_VLM_nodes](https://github.com/gokayfem/ComfyUI_VLM_nodes)
The core functionality is taken from [ComfyUI_VLM_nodes](https://github.com/gokayfem/ComfyUI_VLM_nodes) and belongs to its authors.
## LLaVa nodes
A node that generates text using the LLM model and CLIP by image and prompt with subsequent unloading of the model from memory.
Our LLava nodes support the latest LLM models, and should support future ones (please let us know if any models stop working).
Most of the functionality is taken from this package - [https://github.com/gokayfem/ComfyUI_VLM_nodes](https://github.com/gokayfem/ComfyUI_VLM_nodes)
The core functionality is taken from [ComfyUI_VLM_nodes](https://github.com/gokayfem/ComfyUI_VLM_nodes) and belongs to its authors.
## Image Remove Background based on RemBG
A more improved version of rembg with an extended list of models. Sources of code and packages:
Main code - [https://github.com/Loewen-Hob/rembg-comfyui-node-better](https://github.com/Loewen-Hob/rembg-comfyui-node-better)
RemBG package - [https://github.com/danielgatis/rembg](https://github.com/danielgatis/rembg)
A more improved version of rembg nodes for ComfyUI with an extended list of models.
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
An image autotagger that creates highly relevant tags using fast and ultra-accurate, highly specialized models. More diverse models are planned to be added to the list of models in the future.
This node allows it to be used in cycles and conditions (in places where it is not necessary to execute this node according to the specified conditions), since it is not a node with mandatory execution.
The core functionality is taken from [ComfyUI-WD14-Tagger](https://github.com/pythongosssss/ComfyUI-WD14-Tagger) and belongs to its authors.
## Tag Category Filter nodes
A set of nodes that allow you to filter tags by category. There is an option to remove or leave certain categories of tags, there is a function for defining categories of all tags, there is a function for removing certain tags.
Nodes are very convenient because you can use them to remove unnecessary tags by certain categories, for example, to clean up tags and prepare them for use. You can use this to get certain prompts from an image (for example, if you need a description of only the background from an image - you can get this category of tags if you set the "background" category in Tag Category Keeper).
Most of the functionality is taken from this package - [https://github.com/sugarkwork/comfyui_tag_fillter](https://github.com/sugarkwork/comfyui_tag_fillter)
The core functionality is taken from [comfyui_tag_fillter](https://github.com/sugarkwork/comfyui_tag_fillter) and belongs to its authors.
## Model Unloader nodes
@@ -48,8 +100,33 @@ Quickly creates a background image with a checkerboard pattern according to the
## Simple Float Slider
Simple Float Slider is a handy slider from 0.0001 to 1.0000 to conveniently manage variables in your workflow. The min and max values cannot be changed on the interface (but you can change these values inside Python if you really need to).
Simple Float Slider is a handy slider from 0.0 to 1.0 to conveniently manage variables in your workflow. The min and max values cannot be changed on the interface (but you can change these values inside Python if you really need to).
The pack contains two additional sliders - "Simple Float Slider - Tenths Step" and "Simple Float Slider - Hundredths Step" for working with more precisely defined values in tenths and hundredths (work correctly only if you have not changed the value of "Float widget rounding decimal places" in the ComfyUI settings. If you have changed it, then return the value to 0).
The core functionality is taken from [comfyui-mixlab-nodes](https://github.com/shadowcz007/comfyui-mixlab-nodes) and belongs to its authors.
(C) Level Pixel
## Other nodes
There are a few more nodes in this package that have some unusual uses:
* Preview Image Bridge - only output an image to the screen if there is a connection to the output node. Useful in loops and conditions where the execution of this node is not required due to current conditions (variables).
* Show Text Bridge - only output text to the screen if there is a connection to the output node. Useful in loops and conditions where the execution of this node is not required due to current conditions (variables).
* Show Text - output text to the screen with mandatory execution. The node is executed in any case, whether the output is connected or not.
# Credits
ComfyUI/[ComfyUI](https://github.com/comfyanonymous/ComfyUI) - A powerful and modular stable diffusion GUI.
VLM nodes for ComfyUI/[ComfyUI_VLM_nodes](https://github.com/gokayfem/ComfyUI_VLM_nodes) - Best VLM nodes for ComfyUI.
Tag Filter nodes for ComfyUI/[comfyui_tag_fillter](https://github.com/sugarkwork/comfyui_tag_fillter) - Best tag filter by category nodes for ComfyUI.
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
Licensed under Apache License
+3 -12
View File
@@ -2,19 +2,8 @@ import os
import pkg_resources
import sys
import subprocess
import folder_paths
import importlib
supported_LLava_extensions = set(['.gguf'])
try:
folder_paths.folder_names_and_paths["LLavacheckpoints"] = (folder_paths.folder_names_and_paths["LLavacheckpoints"][0], supported_LLava_extensions)
except:
if not os.path.isdir(os.path.join(folder_paths.models_dir, "LLavacheckpoints")):
os.mkdir(os.path.join(folder_paths.models_dir, "LLavacheckpoints"))
folder_paths.folder_names_and_paths["LLavacheckpoints"] = ([os.path.join(folder_paths.models_dir, "LLavacheckpoints")], supported_LLava_extensions)
def check_requirements_installed(requirements_path):
with open(requirements_path, 'r') as f:
requirements = [pkg_resources.Requirement.parse(line.strip()) for line in f if line.strip()]
@@ -48,12 +37,14 @@ node_list = [
"io.numbers_utils_LP",
"io.folder_workers_LP",
"io.image_loaders_LP",
"io.text_utils_LP",
"io.image_outputs_LP",
"io.text_outputs_LP",
"llm.llm_LP",
"tags.tags_utils_LP",
"text.text_utils_LP",
"unloaders.model_unloaders_LP",
"vlm.llava_LP",
"vlm.autotagger_LP"
]
NODE_CLASS_MAPPINGS = {}
+1 -1
View File
@@ -159,7 +159,7 @@ class ImageRemoveBackground:
RETURN_TYPES = ("IMAGE",)
FUNCTION = "remove_background"
CATEGORY = "image"
CATEGORY = "LevelPixel/Image"
def remove_background(self, image, model_name):
session = new_session(model_name)
+88
View File
@@ -0,0 +1,88 @@
import numpy as np
import os
import sys
from PIL import Image
from PIL.PngImagePlugin import PngInfo
import folder_paths
import random
import json
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy"))
from comfy.cli_args import args
class SaveImage:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
self.prefix_append = ""
self.compress_level = 4
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE", {"tooltip": "The images to save."}),
"filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."})
},
"hidden": {
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "save_images"
OUTPUT_NODE = False
CATEGORY = "LevelPixel/IO"
DESCRIPTION = "Saves the input images to your ComfyUI output directory."
def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
filename_prefix += self.prefix_append
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
results = list()
for (batch_number, image) in enumerate(images):
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = None
if not args.disable_metadata:
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.png"
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=self.compress_level)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
return { "ui": { "images": results }, "result": (images,) }
class PreviewImageForConditions(SaveImage):
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
self.compress_level = 1
@classmethod
def INPUT_TYPES(s):
return {"required":
{"images": ("IMAGE", ), },
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
NODE_CLASS_MAPPINGS = {
"PreviewImageForConditions|LP": PreviewImageForConditions,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PreviewImageForConditions|LP": "Preview Image Bridge [LP]",
}
+64
View File
@@ -28,10 +28,74 @@ class FloatSlider:
number = 1.000000
return (number,)
class TenthsFloatSlider:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"number":("FLOAT", {
"default": 0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"display": "slider"
}),
},
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ('FLOAT',)
FUNCTION = "tenthsFloatSlider"
CATEGORY = "LevelPixel/IO"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def tenthsFloatSlider(self, number):
if number < 0.0:
number = 0.0
elif number > 1.0:
number = 1.0
return (number,)
class HundredthsFloatSlider:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"number":("FLOAT", {
"default": 0,
"min": 0.00,
"max": 1.00,
"step": 0.01,
"display": "slider"
}),
},
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ('FLOAT',)
FUNCTION = "hundredthsFloatSlider"
CATEGORY = "LevelPixel/IO"
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,)
def hundredthsFloatSlider(self, number):
if number < 0.00:
number = 0.00
elif number > 1.00:
number = 1.00
return (number,)
NODE_CLASS_MAPPINGS = {
"SimpleFloatSlider|LP": FloatSlider,
"TenthsSimpleFloatSlider|LP": TenthsFloatSlider,
"HundredthsSimpleFloatSlider|LP": HundredthsFloatSlider,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SimpleFloatSlider|LP": "Simple Float Slider [LP]",
"TenthsSimpleFloatSlider|LP": "Simple Float Slider - Tenths Step [LP]",
"HundredthsSimpleFloatSlider|LP": "Simple Float Slider - Hundredths Step [LP]",
}
+91
View File
@@ -0,0 +1,91 @@
class ShowText:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
INPUT_IS_LIST = True
RETURN_TYPES = ("STRING",)
FUNCTION = "show_text"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
CATEGORY = "LevelPixel/IO"
def show_text(self, text, unique_id=None, extra_pnginfo=None):
if unique_id is not None and extra_pnginfo is not None:
if not isinstance(extra_pnginfo, list):
print("Error: extra_pnginfo is not a list")
elif (
not isinstance(extra_pnginfo[0], dict)
or "workflow" not in extra_pnginfo[0]
):
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
else:
workflow = extra_pnginfo[0]["workflow"]
node = next(
(x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])),
None,
)
if node:
node["widgets_values"] = [text]
return {"ui": {"text": text}, "result": (text,)}
class ShowTextBridge:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
INPUT_IS_LIST = True
RETURN_TYPES = ("STRING",)
FUNCTION = "show_text_bridge"
OUTPUT_NODE = False
OUTPUT_IS_LIST = (True,)
CATEGORY = "LevelPixel/IO"
def show_text_bridge(self, text, unique_id=None, extra_pnginfo=None):
if unique_id is not None and extra_pnginfo is not None:
if not isinstance(extra_pnginfo, list):
print("Error: extra_pnginfo is not a list")
elif (
not isinstance(extra_pnginfo[0], dict)
or "workflow" not in extra_pnginfo[0]
):
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
else:
workflow = extra_pnginfo[0]["workflow"]
node = next(
(x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])),
None,
)
if node:
node["widgets_values"] = [text]
return {"ui": {"text": text}, "result": (text,)}
NODE_CLASS_MAPPINGS = {
"ShowText|LP": ShowText,
"ShowTextBridge|LP": ShowTextBridge,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ShowText|LP": "Show Text [LP]",
"ShowTextBridge|LP": "Show Text Bridge [LP]",
}
-48
View File
@@ -1,48 +0,0 @@
class ShowTextLP:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
INPUT_IS_LIST = True
RETURN_TYPES = ("STRING",)
FUNCTION = "show_text_lp"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
CATEGORY = "LevelPixel/IO"
def show_text_lp(self, text, unique_id=None, extra_pnginfo=None):
if unique_id is not None and extra_pnginfo is not None:
if not isinstance(extra_pnginfo, list):
print("Error: extra_pnginfo is not a list")
elif (
not isinstance(extra_pnginfo[0], dict)
or "workflow" not in extra_pnginfo[0]
):
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
else:
workflow = extra_pnginfo[0]["workflow"]
node = next(
(x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])),
None,
)
if node:
node["widgets_values"] = [text]
return {"ui": {"text": text}, "result": (text,)}
NODE_CLASS_MAPPINGS = {
"ShowText|LP": ShowTextLP,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ShowText|LP": "Show Text [LP]",
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "Autotagger",
"logging": false,
"settings": {
"model": "wd-eva02-large-tagger-v3",
"threshold": 0.35,
"character_threshold": 0.85,
"exclude_tags": "",
"ortProviders": ["CUDAExecutionProvider", "CPUExecutionProvider"],
"HF_ENDPOINT": "https://huggingface.co"
},
"models": {
"wd-eva02-large-tagger-v3": "{HF_ENDPOINT}/SmilingWolf/wd-eva02-large-tagger-v3",
"wd-vit-tagger-v3": "{HF_ENDPOINT}/SmilingWolf/wd-vit-tagger-v3",
"wd-swinv2-tagger-v3": "{HF_ENDPOINT}/SmilingWolf/wd-swinv2-tagger-v3",
"wd-convnext-tagger-v3": "{HF_ENDPOINT}/SmilingWolf/wd-convnext-tagger-v3",
"wd-v1-4-moat-tagger-v2": "{HF_ENDPOINT}/SmilingWolf/wd-v1-4-moat-tagger-v2",
"wd-v1-4-convnextv2-tagger-v2": "{HF_ENDPOINT}/SmilingWolf/wd-v1-4-convnextv2-tagger-v2",
"wd-v1-4-convnext-tagger-v2": "{HF_ENDPOINT}/SmilingWolf/wd-v1-4-convnext-tagger-v2",
"wd-v1-4-convnext-tagger": "{HF_ENDPOINT}/SmilingWolf/wd-v1-4-convnext-tagger",
"wd-v1-4-vit-tagger-v2": "{HF_ENDPOINT}/SmilingWolf/wd-v1-4-vit-tagger-v2",
"wd-v1-4-swinv2-tagger-v2": "{HF_ENDPOINT}/SmilingWolf/wd-v1-4-swinv2-tagger-v2",
"wd-v1-4-vit-tagger": "{HF_ENDPOINT}/SmilingWolf/wd-v1-4-vit-tagger",
"Z3D-E621-Convnext": "{HF_ENDPOINT}/silveroxides/Z3D-E621-Convnext"
}
}
+448
View File
@@ -0,0 +1,448 @@
# https://huggingface.co/spaces/SmilingWolf/wd-v1-4-tags
import comfy.utils
import asyncio
import aiohttp
import numpy as np
import csv
import os
import sys
import onnxruntime as ort
from onnxruntime import InferenceSession
from PIL import Image
from server import PromptServer
from aiohttp import web
import folder_paths
import json
import shutil
import inspect
from tqdm import tqdm
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy"))
supported_autotaggers_extensions = set(['.onnx'])
try:
folder_paths.folder_names_and_paths["autotaggers"] = (folder_paths.folder_names_and_paths["autotaggers"][0], supported_autotaggers_extensions)
except:
if not os.path.isdir(os.path.join(folder_paths.models_dir, "autotaggers")):
os.mkdir(os.path.join(folder_paths.models_dir, "autotaggers"))
folder_paths.folder_names_and_paths["autotaggers"] = ([os.path.join(folder_paths.models_dir, "autotaggers")], supported_autotaggers_extensions)
config = None
def is_logging_enabled():
config = get_extension_config()
if "logging" not in config:
return False
return config["logging"]
def log(message, type=None, always=False):
if not always and not is_logging_enabled():
return
if type is not None:
message = f"[{type}] {message}"
name = get_extension_config()["name"]
print(f"(LevelPixel:{name}) {message}")
def get_ext_dir(subpath=None, mkdir=False):
dir = os.path.dirname(__file__)
if subpath is not None:
dir = os.path.join(dir, subpath)
dir = os.path.abspath(dir)
if mkdir and not os.path.exists(dir):
os.makedirs(dir)
return dir
def get_comfy_dir(subpath=None):
dir = os.path.dirname(inspect.getfile(PromptServer))
if subpath is not None:
dir = os.path.join(dir, subpath)
dir = os.path.abspath(dir)
return dir
def get_web_ext_dir():
config = get_extension_config()
name = config["name"]
dir = get_comfy_dir("web/extensions/levelpixel")
if not os.path.exists(dir):
os.makedirs(dir)
dir += "/" + name
return dir
def get_extension_config(reload=False):
global config
if reload == False and config is not None:
return config
config_path = get_ext_dir("autotagger_LP.user.json")
if not os.path.exists(config_path):
config_path = get_ext_dir("autotagger_LP.json")
if not os.path.exists(config_path):
log("Missing autotagger_LP.json and autotagger_LP.user.json, this extension may not work correctly. Please reinstall the extension.",
type="ERROR", always=True)
print(f"Extension path: {get_ext_dir()}")
return {"name": "Unknown", "version": -1}
with open(config_path, "r") as f:
config = json.loads(f.read())
return config
def link_js(src, dst):
src = os.path.abspath(src)
dst = os.path.abspath(dst)
if os.name == "nt":
try:
import _winapi
_winapi.CreateJunction(src, dst)
return True
except:
pass
try:
os.symlink(src, dst)
return True
except:
import logging
logging.exception('')
return False
def is_junction(path):
if os.name != "nt":
return False
try:
return bool(os.readlink(path))
except OSError:
return False
def install_js():
src_dir = get_ext_dir("web/js")
if not os.path.exists(src_dir):
log("No JS")
return
should_install = should_install_js()
if should_install:
log("it looks like you're running an old version of ComfyUI that requires manual setup of web files, it is recommended you update your installation.", "warning", True)
dst_dir = get_web_ext_dir()
linked = os.path.islink(dst_dir) or is_junction(dst_dir)
if linked or os.path.exists(dst_dir):
if linked:
if should_install:
log("JS already linked")
else:
os.unlink(dst_dir)
log("JS unlinked, PromptServer will serve extension")
elif not should_install:
shutil.rmtree(dst_dir)
log("JS deleted, PromptServer will serve extension")
return
if not should_install:
log("JS skipped, PromptServer will serve extension")
return
if link_js(src_dir, dst_dir):
log("JS linked")
return
log("Copying JS files")
shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True)
def should_install_js():
return not hasattr(PromptServer.instance, "supports") or "custom_nodes_from_web" not in PromptServer.instance.supports
def init(check_imports):
log("Init")
if check_imports is not None:
import importlib.util
for imp in check_imports:
spec = importlib.util.find_spec(imp)
if spec is None:
log(f"{imp} is required, please check requirements are installed.", type="ERROR", always=True)
return False
install_js()
return True
async def download_to_file(url, destination, update_callback, is_ext_subpath=True, session=None):
close_session = False
if session is None:
close_session = True
loop = None
try:
loop = asyncio.get_event_loop()
except:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
session = aiohttp.ClientSession(loop=loop)
if is_ext_subpath:
destination = get_ext_dir(destination)
try:
proxy = os.getenv("HTTP_PROXY") or os.getenv("http_proxy")
print("proxy:", proxy)
proxy_auth = None
if proxy:
proxy_auth = aiohttp.BasicAuth(os.getenv("PROXY_USER", ""), os.getenv("PROXY_PASS", ""))
async with session.get(url, proxy=proxy, proxy_auth=proxy_auth) as response:
size = int(response.headers.get('content-length', 0)) or None
with tqdm(
unit='B', unit_scale=True, miniters=1, desc=url.split('/')[-1], total=size,
) as progressbar:
with open(destination, mode='wb') as f:
perc = 0
async for chunk in response.content.iter_chunked(2048):
f.write(chunk)
progressbar.update(len(chunk))
if update_callback is not None and progressbar.total is not None and progressbar.total != 0:
last = perc
perc = round(progressbar.n / progressbar.total, 2)
if perc != last:
last = perc
await update_callback(perc)
finally:
if close_session and session is not None:
await session.close()
def wait_for_async(async_fn, loop=None):
return asyncio.run(async_fn())
def update_node_status(client_id, node, text, progress=None):
if client_id is None:
client_id = PromptServer.instance.client_id
if client_id is None:
return
PromptServer.instance.send_sync("levelpixel/update_status", {
"node": node,
"progress": progress,
"text": text
}, client_id)
async def update_node_status_async(client_id, node, text, progress=None):
if client_id is None:
client_id = PromptServer.instance.client_id
if client_id is None:
return
await PromptServer.instance.send("levelpixel/update_status", {
"node": node,
"progress": progress,
"text": text
}, client_id)
config_autotagger = get_extension_config()
defaults = {
"model": "wd-eva02-large-tagger-v3",
"threshold": 0.35,
"character_threshold": 0.85,
"replace_underscore": False,
"trailing_comma": False,
"exclude_tags": "",
"ortProviders": ["CUDAExecutionProvider", "CPUExecutionProvider"],
"HF_ENDPOINT": "https://huggingface.co"
}
defaults.update(config_autotagger.get("settings", {}))
models_dir = folder_paths.get_folder_paths("autotaggers")[0]
if not os.path.exists(models_dir):
os.makedirs(models_dir)
known_models = list(config_autotagger["models"].keys())
#log("Available ORT providers: " + ", ".join(ort.get_available_providers()), "DEBUG", True)
#log("Using ORT providers: " + ", ".join(defaults["ortProviders"]), "DEBUG", True)
def get_installed_models():
models = filter(lambda x: x.endswith(".onnx"), os.listdir(models_dir))
models = [m for m in models if os.path.exists(os.path.join(models_dir, os.path.splitext(m)[0] + ".csv"))]
return models
async def tag(image, model_name, threshold=0.35, character_threshold=0.85, exclude_tags="", replace_underscore=True, trailing_comma=False, client_id=None, node=None):
if model_name.endswith(".onnx"):
model_name = model_name[0:-5]
installed = list(get_installed_models())
if not any(model_name + ".onnx" in s for s in installed):
await download_model(model_name, client_id, node)
name = os.path.join(models_dir, model_name + ".onnx")
model = InferenceSession(name, providers=defaults["ortProviders"])
input = model.get_inputs()[0]
height = input.shape[1]
# Reduce to max size and pad with white
ratio = float(height)/max(image.size)
new_size = tuple([int(x*ratio) for x in image.size])
image = image.resize(new_size, Image.LANCZOS)
square = Image.new("RGB", (height, height), (255, 255, 255))
square.paste(image, ((height-new_size[0])//2, (height-new_size[1])//2))
image = np.array(square).astype(np.float32)
image = image[:, :, ::-1] # RGB -> BGR
image = np.expand_dims(image, 0)
# Read all tags from csv and locate start of each category
tags = []
general_index = None
character_index = None
with open(os.path.join(models_dir, model_name + ".csv")) as f:
reader = csv.reader(f)
next(reader)
for row in reader:
if general_index is None and row[2] == "0":
general_index = reader.line_num - 2
elif character_index is None and row[2] == "4":
character_index = reader.line_num - 2
if replace_underscore:
tags.append(row[1].replace("_", " "))
else:
tags.append(row[1])
label_name = model.get_outputs()[0].name
probs = model.run([label_name], {input.name: image})[0]
result = list(zip(tags, probs[0]))
# rating = max(result[:general_index], key=lambda x: x[1])
general = [item for item in result[general_index:character_index] if item[1] > threshold]
character = [item for item in result[character_index:] if item[1] > character_threshold]
all = character + general
remove = [s.strip() for s in exclude_tags.lower().split(",")]
all = [tag for tag in all if tag[0] not in remove]
res = ("" if trailing_comma else ", ").join((item[0].replace("(", "\\(").replace(")", "\\)") + (", " if trailing_comma else "") for item in all))
print(res)
return res
async def download_model(model, client_id, node):
hf_endpoint = os.getenv("HF_ENDPOINT", defaults["HF_ENDPOINT"])
if not hf_endpoint.startswith("https://"):
hf_endpoint = f"https://{hf_endpoint}"
if hf_endpoint.endswith("/"):
hf_endpoint = hf_endpoint.rstrip("/")
url = config_autotagger["models"][model]
url = url.replace("{HF_ENDPOINT}", hf_endpoint)
url = f"{url}/resolve/main/"
async with aiohttp.ClientSession(loop=asyncio.get_event_loop()) as session:
async def update_callback(perc):
nonlocal client_id
message = ""
if perc < 100:
message = f"Downloading {model}"
update_node_status(client_id, node, message, perc)
try:
await download_to_file(
f"{url}model.onnx", os.path.join(models_dir,f"{model}.onnx"), update_callback, session=session)
await download_to_file(
f"{url}selected_tags.csv", os.path.join(models_dir,f"{model}.csv"), update_callback, session=session)
except aiohttp.client_exceptions.ClientConnectorError as err:
log("Unable to download model. Download files manually or try using a HF mirror/proxy website by setting the environment variable HF_ENDPOINT=https://.....", "ERROR", True)
raise
update_node_status(client_id, node, None)
return web.Response(status=200)
@PromptServer.instance.routes.get("/levelpixel/autotagger/tag")
async def get_tags(request):
if "filename" not in request.rel_url.query:
return web.Response(status=404)
type = request.query.get("type", "output")
if type not in ["output", "input", "temp"]:
return web.Response(status=400)
target_dir = get_comfy_dir(type)
image_path = os.path.abspath(os.path.join(
target_dir, request.query.get("subfolder", ""), request.query["filename"]))
c = os.path.commonpath((image_path, target_dir))
if os.path.commonpath((image_path, target_dir)) != target_dir:
return web.Response(status=403)
if not os.path.isfile(image_path):
return web.Response(status=404)
image = Image.open(image_path)
models = get_installed_models()
default = defaults["model"] + ".onnx"
model = default if default in models else models[0]
return web.json_response(await tag(image, model, client_id=request.rel_url.query.get("clientId", ""), node=request.rel_url.query.get("node", "")))
class Autotagger:
@classmethod
def INPUT_TYPES(s):
extra = [name for name, _ in (os.path.splitext(m) for m in get_installed_models()) if name not in known_models]
models = known_models + extra
return {"required": {
"image": ("IMAGE", ),
"model": (models, { "default": defaults["model"] }),
"threshold": ("FLOAT", {"default": defaults["threshold"], "min": 0.0, "max": 1, "step": 0.05}),
"character_threshold": ("FLOAT", {"default": defaults["character_threshold"], "min": 0.0, "max": 1, "step": 0.05}),
"replace_underscore": ("BOOLEAN", {"default": defaults["replace_underscore"]}),
"trailing_comma": ("BOOLEAN", {"default": defaults["trailing_comma"]}),
"exclude_tags": ("STRING", {"default": defaults["exclude_tags"]}),
}}
RETURN_TYPES = ("STRING",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "tag"
OUTPUT_NODE = False
CATEGORY = "LevelPixel/VLM"
def tag(self, image, model, threshold, character_threshold, exclude_tags="", replace_underscore=False, trailing_comma=False):
tensor = image*255
tensor = np.array(tensor, dtype=np.uint8)
pbar = comfy.utils.ProgressBar(tensor.shape[0])
tags = []
for i in range(tensor.shape[0]):
image = Image.fromarray(tensor[i])
tags.append(wait_for_async(lambda: tag(image, model, threshold, character_threshold, exclude_tags, replace_underscore, trailing_comma)))
pbar.update(1)
return {"ui": {"tags": tags}, "result": (tags,)}
NODE_CLASS_MAPPINGS = {
"Autotagger|LP": Autotagger,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Autotagger|LP": "Autotagger [LP]",
}
+6 -6
View File
@@ -37,7 +37,7 @@ class LLavaLoader:
RETURN_NAMES = ("model",)
FUNCTION = "load_llava_checkpoint"
CATEGORY = "LevelPixel/LLava"
CATEGORY = "LevelPixel/VLM"
def load_llava_checkpoint(self, ckpt_name, max_ctx, gpu_layers, n_threads, clip ):
ckpt_path = folder_paths.get_full_path("LLavacheckpoints", ckpt_name)
llm = Llama(model_path = ckpt_path, chat_handler=clip,offload_kqv=True, f16_kv=True,
@@ -59,7 +59,7 @@ class LLavaClipLoader:
RETURN_NAMES = ("clip", )
FUNCTION = "load_clip_checkpoint"
CATEGORY = "LevelPixel/LLava"
CATEGORY = "LevelPixel/VLM"
def load_clip_checkpoint(self, clip_name):
clip_path = folder_paths.get_full_path("LLavacheckpoints", clip_name)
clip = Llava16ChatHandler(clip_model_path = clip_path, verbose=False)
@@ -82,7 +82,7 @@ class LLavaSamplerSimple:
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_text_simple"
CATEGORY = "LevelPixel/LLava"
CATEGORY = "LevelPixel/VLM"
def generate_text_simple(self, image, prompt, model, temperature):
@@ -138,7 +138,7 @@ class LLavaSamplerAdvanced:
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_text_advanced"
CATEGORY = "LevelPixel/LLava"
CATEGORY = "LevelPixel/VLM"
def generate_text_advanced(self, image, system_msg, prompt, model, max_tokens, temperature, top_p,
frequency_penalty, presence_penalty, repeat_penalty, top_k,seed):
@@ -201,7 +201,7 @@ class LLavaSimple:
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_text_full_simple"
CATEGORY = "LevelPixel/LLava"
CATEGORY = "LevelPixel/VLM"
def generate_text_full_simple(self, ckpt_name, clip_name, max_ctx, gpu_layers, n_threads, image, prompt, temperature, unload):
@@ -285,7 +285,7 @@ class LLavaAdvanced:
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_text_full_advanced"
CATEGORY = "LevelPixel/LLava"
CATEGORY = "LevelPixel/VLM"
def generate_text_full_advanced(self, ckpt_name, clip_name, max_ctx, gpu_layers, n_threads, image,
system_msg, prompt, max_tokens, temperature, top_p, top_k, frequency_penalty,
+3 -3
View File
@@ -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."
version = "1.0.6"
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"
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"]
@@ -12,4 +12,4 @@ Repository = "https://github.com/LevelPixel/ComfyUI-LevelPixel"
PublisherId = "Level Pixel"
DisplayName = "ComfyUI_LevelPixel"
Icon = ""
Models = [{location = "/checkpoints/model.safetensor", model_url = "https://example.com/model.zip"}]
Models = [{location = "/checkpoints/model.safetensors", model_url = "https://example.com/model.zip"}]
+141
View File
@@ -0,0 +1,141 @@
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
import { api } from "../../../scripts/api.js";
class LevelPixel {
constructor() {
if (!window.__levelpixel__) {
window.__levelpixel__ = Symbol("__levelpixel__");
}
this.symbol = window.__levelpixel__;
}
getState(node) {
return node[this.symbol] || {};
}
setState(node, state) {
node[this.symbol] = state;
app.canvas.setDirty(true);
}
addStatusTagHandler(nodeType) {
if (nodeType[this.symbol]?.statusTagHandler) {
return;
}
if (!nodeType[this.symbol]) {
nodeType[this.symbol] = {};
}
nodeType[this.symbol] = {
statusTagHandler: true,
};
api.addEventListener("levelpixel/update_status", ({ detail }) => {
let { node, progress, text } = detail;
const n = app.graph.getNodeById(+(node || app.runningNodeId));
if (!n) return;
const state = this.getState(n);
state.status = Object.assign(state.status || {}, { progress: text ? progress : null, text: text || null });
this.setState(n, state);
});
const self = this;
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
const r = onDrawForeground?.apply?.(this, arguments);
const state = self.getState(this);
if (!state?.status?.text) {
return r;
}
const { fgColor, bgColor, text, progress, progressColor } = { ...state.status };
ctx.save();
ctx.font = "12px sans-serif";
const sz = ctx.measureText(text);
ctx.fillStyle = bgColor || "dodgerblue";
ctx.beginPath();
ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5);
ctx.fill();
if (progress) {
ctx.fillStyle = progressColor || "green";
ctx.beginPath();
ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, (sz.width + 12) * progress, 20, 5);
ctx.fill();
}
ctx.fillStyle = fgColor || "#fff";
ctx.fillText(text, 6, -LiteGraph.NODE_TITLE_HEIGHT - 6);
ctx.restore();
return r;
};
}
}
const levelpixel = new LevelPixel();
app.registerExtension({
name: "levelpixel.Autotagger",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
levelpixel.addStatusTagHandler(nodeType);
if (nodeData.name === "Autotagger|LevelPixel") {
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
const r = onExecuted?.apply?.(this, arguments);
const pos = this.widgets.findIndex((w) => w.name === "tags");
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.tags) {
const w = ComfyWidgets["STRING"](this, "tags", ["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: "Autotagger",
callback: async () => {
let src = img.src;
src = src.replace("/view?", `/levelpixel/autotagger/tag?node=${this.id}&clientId=${api.clientId}&`);
const res = await (await fetch(src)).json();
alert(res);
},
});
}
return r;
};
}
},
});