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:
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
@@ -1,30 +1,82 @@
|
||||
## Level Pixel nodes for ComfyUI
|
||||
|
||||

|
||||
|
||||
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
@@ -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 = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]",
|
||||
}
|
||||
|
||||
@@ -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]",
|
||||
}
|
||||
@@ -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]",
|
||||
}
|
||||
@@ -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]",
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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]",
|
||||
}
|
||||
@@ -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
@@ -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"}]
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user