Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38e89f9567 | ||
|
|
f752500e74 | ||
|
|
11af9469ee | ||
|
|
abb9b39448 | ||
|
|
2e889c7018 | ||
|
|
52157dff5a | ||
|
|
8c18075e9f | ||
|
|
e0ec5dee30 | ||
|
|
c09e56b21f | ||
|
|
763152c041 | ||
|
|
c85b302a61 | ||
|
|
6484968fbe | ||
|
|
9a212d13e7 | ||
|
|
b02bea5480 | ||
|
|
8c4b7941a6 | ||
|
|
070751dbf1 | ||
|
|
41f6277830 | ||
|
|
03ddfbfe66 | ||
|
|
885a1c8371 | ||
|
|
8656b2ebaf | ||
|
|
c3759e7b30 | ||
|
|
236e230515 | ||
|
|
23ef04b1cb | ||
|
|
a95cba4e3b | ||
|
|
40f09582e2 | ||
|
|
6635d30566 | ||
|
|
5cf49c9565 | ||
|
|
1e8ed1e16f | ||
|
|
64113afd7f | ||
|
|
2bd6769b78 | ||
|
|
96a69f7cbe | ||
|
|
9d8b79844f | ||
|
|
f3815b7ce4 | ||
|
|
d5641ba745 | ||
|
|
5b5eaf3a81 | ||
|
|
517b8a8a12 |
@@ -0,0 +1,21 @@
|
|||||||
|
name: Publish to Comfy registry
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "pyproject.toml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-node:
|
||||||
|
name: Publish Custom Node to registry
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Publish Custom Node
|
||||||
|
uses: Comfy-Org/publish-node-action@main
|
||||||
|
with:
|
||||||
|
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||||
|
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||||
@@ -1,38 +1,92 @@
|
|||||||
# ComfyUI-Lora-Auto-Trigger-Words
|
# ComfyUI-Lora-Auto-Trigger-Words
|
||||||
|
|
||||||
This project is a fork of https://github.com/Extraltodeus/LoadLoraWithTags
|
This project is a fork of https://github.com/Extraltodeus/LoadLoraWithTags
|
||||||
The aim of these custom nodes is to get an _easy_ access to the tags used to trigger a lora.
|
The aim of these custom nodes is to get an _easy_ access to the **trigger words** (or **tags**) used to trigger a lora.
|
||||||
This project is compatible with Stacked Loras from https://github.com/LucianoCirino/efficiency-nodes-comfyui/releases
|
|
||||||
I talk about **lora**, but works with **lycoris** too.
|
*Note that trigger words and tags are used interchangeably in this readme and means the same thing.*
|
||||||
|
|
||||||
|
This project is compatible with Stacked Loras from https://github.com/jags111/efficiency-nodes-comfyui
|
||||||
|
When I talk about **lora**, I also mean **lycoris** too.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
Some of this project nodes depends on https://github.com/pythongosssss/ComfyUI-Custom-Scripts :
|
Some of this project nodes depends on https://github.com/pythongosssss/ComfyUI-Custom-Scripts :
|
||||||
- LoraLoaderAdvanced
|
- LoraLoaderAdvanced
|
||||||
- LoraLoaderStackedAdvanced
|
- LoraLoaderStackedAdvanced
|
||||||
They get their vanilla equivalents.
|
- `View info...`
|
||||||
|
|
||||||
|
They get their vanilla equivalents with less features
|
||||||
|
|
||||||
Overall, Custom-Scripts is recommended to be able to know the content of the tag lists with the node `showText`
|
Overall, Custom-Scripts is recommended to be able to know the content of the tag lists with the node `showText`
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
### Main nodes
|
### Lora Trigger Words
|
||||||
|
Lora trigger words are imported from two sources :
|
||||||
|
- Civitai api (only for civitai models)
|
||||||
|
|
||||||
|

|
||||||
|
- Model training metadata (when available)
|
||||||
|
|
||||||
#### Vanilla vs Advanced
|
#### Vanilla vs Advanced
|
||||||
Vanilla refers to nodes that have no lora preview from the menu, nor the lora list. But the features provided are the same.
|
> Vanilla refers to nodes that have no lora preview from the menu, nor the lora list. But the features provided are the same.
|
||||||
|
|
||||||

|

|
||||||
#### Nodes
|
### Nodes
|
||||||
- LoraLoader (Vanilla or Advanced)
|
#### LoraLoader (Vanilla and Advanced)
|
||||||
- LoraLoaderStacked (Vanilla or Avanced). The stacked lora input is optional.
|

|
||||||
Allow to load a lora, either the normal way, or the efficiency-nodes way.
|
INPUT
|
||||||
These loaders have two custom outputs:
|
- `override_lora_name` (optional): Used to ignore the field `lora_name` and use the name passed. Should use [LoraListNames](#loralistnames) or the `lora_name` output.
|
||||||
- civitai_tags_list: a python list of the tags related to this lora on civitai
|
|
||||||
- meta_tags_list: a python list of the tags used for training the lora embeded in it (if any)
|
FIELDS
|
||||||
This outputs needs to be filtered by two othere nodes:
|
- `force_fetch`: Force the civitai fetching of data even if there is already something saved
|
||||||
- TagsFormater: Helper to show the available tag and their indexes
|
- `enable_preview`: Toggle on/off the saved lora preview if any (only in advanced)
|
||||||
- tagsSelector: allow to filter tags and apply a weight to it.
|
- `append_lora_if_empty`: Add the name of the lora to the list of tags if the list is empty
|
||||||
- TagSelector contains four parameters. First the selector (see Filtering next)
|
|
||||||
- The weight `(tag:weight)`
|
OUTPUT
|
||||||
- The boolean `ensure_comma`. To properly append comma if a prefix or suffix is added.
|
- `civitai_tags_list`: a python list of the tags related to this lora on civitai
|
||||||
- The boolean `append_loraname_if_empty` which will add the name of the lora in the list of outputs if they are empty.
|
- `meta_tags_list`: a python list of the tags used for training the lora embeded in it (if any)
|
||||||
#### Filtering
|
- `lora_name`: the name of the current selected lora
|
||||||
|
#### LoraLoaderStacked (Vanilla and Avanced).
|
||||||
|

|
||||||
|
INPUT
|
||||||
|
- `lora_stack` (optional): another stack of lora.
|
||||||
|
- `override_lora_name` (optional): Used to ignore the field `lora_name` and use the name passed. Should use [LoraListNames](#loralistnames) or the `lora_name` output.
|
||||||
|
|
||||||
|
FIELDS
|
||||||
|
- `force_fetch`: Force the civitai fetching of data even if there is already something saved
|
||||||
|
- `enable_preview`: Toggle on/off the saved lora preview if any (only in advanced)
|
||||||
|
- `append_lora_if_empty`: Add the name of the lora to the list of tags if the list is empty
|
||||||
|
|
||||||
|
OUTPUT
|
||||||
|
- `civitai_tags_list`: a python list of the tags related to this lora on civitai
|
||||||
|
- `meta_tags_list`: a python list of the tags used for training the lora embeded in it (if any)
|
||||||
|
- `lora_name`: the name of the current selected lora
|
||||||
|
#### LoraTagsOnly
|
||||||
|

|
||||||
|
To get the tags without using the lora.
|
||||||
|
- `override_lora_name` (optional): Used to ignore the field `lora_name` and use the name passed. Should use [LoraListNames](#loralistnames) or the `lora_name` output.
|
||||||
|
|
||||||
|
OUTPUT
|
||||||
|
- `civitai_tags_list`: a python list of the tags related to this lora on civitai
|
||||||
|
- `meta_tags_list`: a python list of the tags used for training the lora embeded in it (if any)
|
||||||
|
- `lora_name`: the name of the current selected lora
|
||||||
|
|
||||||
|
#### TagsFormater
|
||||||
|

|
||||||
|
Helper to show the available tag and their indexes. Tags are sorted by training frequence. The more a tag was used, the higher in the list it is. Works for both `civitai_tags_list` and `meta_tags_list`
|
||||||
|
|
||||||
|
#### TagsSelector
|
||||||
|

|
||||||
|
Allow to filter tags and apply a weight to it.
|
||||||
|
TagSelector contains four parameters.
|
||||||
|
- `selector` (see the [Filtering](#filtering) section next)
|
||||||
|
- `weight`: to format the tag like `(tag:weight)`. Default set to 1 without the weight like `tag`.
|
||||||
|
- `ensure_comma`. To properly append comma if a prefix or suffix is added.
|
||||||
|
|
||||||
|
#### LoraListNames
|
||||||
|

|
||||||
|
List all the existing lora names. It is used as an input for `override_lora_name`
|
||||||
|
|
||||||
|
### Filtering
|
||||||
The format is simple. It's the same as python list index, but can select multiple index or ranges of indexes separated by comas.
|
The format is simple. It's the same as python list index, but can select multiple index or ranges of indexes separated by comas.
|
||||||
`Ex: 0, 3, 5:8, -8:`
|
`Ex: 0, 3, 5:8, -8:`
|
||||||
- Select a specific list of indexes: `0, 2, 3, 15`...
|
- Select a specific list of indexes: `0, 2, 3, 15`...
|
||||||
@@ -42,6 +96,18 @@ The format is simple. It's the same as python list index, but can select multipl
|
|||||||
- You can use negative indexes. Like `-1` to select the last tag
|
- You can use negative indexes. Like `-1` to select the last tag
|
||||||
- By default `:` selects everything
|
- By default `:` selects everything
|
||||||
|
|
||||||
|
### View Info
|
||||||
|

|
||||||
|
|
||||||
|
Pythongossss's [View Info...](https://github.com/pythongosssss/ComfyUI-Custom-Scripts?tab=readme-ov-file#checkpointloraembedding-info) feature from ComfyUI-Custom-Scripts
|
||||||
|
|
||||||
|
To enable this feature go into Settings > Pysssss > ModelInfo > 🐍 Model Info - Lora Nodes/Widgets
|
||||||
|
And add the following at the end of the line:
|
||||||
|
```
|
||||||
|
LoraLoaderVanilla.lora_name,LoraLoaderStackedVanilla.lora_name,LoraLoaderAdvanced.lora_name,LoraLoaderStackedAdvanced.lora_name,LoraTagsOnly.lora_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
#### Example of normal workflow
|
#### Example of normal workflow
|
||||||

|

|
||||||
|
|
||||||
@@ -55,5 +121,5 @@ Lora Stack can also be chained together to load multiple loras into an efficient
|
|||||||
|
|
||||||
### Side nodes I made and kept here
|
### Side nodes I made and kept here
|
||||||
- FusionText: takes two text input and join them together
|
- FusionText: takes two text input and join them together
|
||||||
- Randomizer: takes two couples text+lorastack and return randomly one them
|
- Randomizer: takes two couples text+lorastack and return randomly one of them
|
||||||
- TextInputBasic: just a text input with two additional input for text chaining
|
- TextInputBasic: just a text input with two additional input for text chaining
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ from .nodes_utils import NODE_CLASS_MAPPINGS as nu_NCM
|
|||||||
|
|
||||||
NODE_CLASS_MAPPINGS = dict(na_NCM, **nu_NCM)
|
NODE_CLASS_MAPPINGS = dict(na_NCM, **nu_NCM)
|
||||||
#NODE_DISPLAY_NAME_MAPPINGS = dict(na_NDNM, **nu_NDNM)
|
#NODE_DISPLAY_NAME_MAPPINGS = dict(na_NDNM, **nu_NDNM)
|
||||||
__all__ = ["NODE_CLASS_MAPPINGS"]#, "NODE_DISPLAY_NAME_MAPPINGS"]
|
WEB_DIRECTORY = "./web"
|
||||||
|
__all__ = ["NODE_CLASS_MAPPINGS", "WEB_DIRECTORY"]#, "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||||
|
|||||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -14,7 +14,6 @@ class LoraLoaderVanilla:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"model": ("MODEL",),
|
"model": ("MODEL",),
|
||||||
"clip": ("CLIP", ),
|
|
||||||
"lora_name": (LORA_LIST, ),
|
"lora_name": (LORA_LIST, ),
|
||||||
"strength_model": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
"strength_model": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||||
"strength_clip": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
"strength_clip": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
||||||
@@ -22,16 +21,19 @@ class LoraLoaderVanilla:
|
|||||||
"append_loraname_if_empty": ("BOOLEAN", {"default": False}),
|
"append_loraname_if_empty": ("BOOLEAN", {"default": False}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
|
"clip": ("CLIP", ),
|
||||||
"override_lora_name":("STRING", {"forceInput": True}),
|
"override_lora_name":("STRING", {"forceInput": True}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("MODEL", "CLIP", "LIST", "LIST")
|
RETURN_TYPES = ("MODEL", "CLIP", "LIST", "LIST", "STRING")
|
||||||
RETURN_NAMES = ("MODEL", "CLIP", "civitai_tags_list", "meta_tags_list")
|
RETURN_NAMES = ("MODEL", "CLIP", "civitai_tags_list", "meta_tags_list", "lora_name")
|
||||||
FUNCTION = "load_lora"
|
FUNCTION = "load_lora"
|
||||||
CATEGORY = "autotrigger"
|
CATEGORY = "autotrigger"
|
||||||
|
|
||||||
def load_lora(self, model, clip, lora_name, strength_model, strength_clip, force_fetch, append_loraname_if_empty, override_lora_name=""):
|
def load_lora(self, model, lora_name, strength_model, strength_clip, force_fetch, append_loraname_if_empty, clip=None, override_lora_name=""):
|
||||||
|
if clip is None:
|
||||||
|
strength_clip=0
|
||||||
if override_lora_name != "":
|
if override_lora_name != "":
|
||||||
lora_name = override_lora_name
|
lora_name = override_lora_name
|
||||||
meta_tags_list = sort_tags_by_frequency(get_metadata(lora_name, "loras"))
|
meta_tags_list = sort_tags_by_frequency(get_metadata(lora_name, "loras"))
|
||||||
@@ -55,8 +57,7 @@ class LoraLoaderVanilla:
|
|||||||
self.loaded_lora = (lora_path, lora)
|
self.loaded_lora = (lora_path, lora)
|
||||||
|
|
||||||
model_lora, clip_lora = load_lora_for_models(model, clip, lora, strength_model, strength_clip)
|
model_lora, clip_lora = load_lora_for_models(model, clip, lora, strength_model, strength_clip)
|
||||||
|
return (model_lora, clip_lora, civitai_tags_list, meta_tags_list, lora_name)
|
||||||
return (model_lora, clip_lora, civitai_tags_list, meta_tags_list)
|
|
||||||
|
|
||||||
class LoraLoaderStackedVanilla:
|
class LoraLoaderStackedVanilla:
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -75,8 +76,8 @@ class LoraLoaderStackedVanilla:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("LIST", "LIST", "LORA_STACK",)
|
RETURN_TYPES = ("LIST", "LIST", "LORA_STACK", "STRING")
|
||||||
RETURN_NAMES = ("civitai_tags_list", "meta_tags_list", "LORA_STACK",)
|
RETURN_NAMES = ("civitai_tags_list", "meta_tags_list", "LORA_STACK", "lora_name")
|
||||||
FUNCTION = "set_stack"
|
FUNCTION = "set_stack"
|
||||||
#OUTPUT_NODE = False
|
#OUTPUT_NODE = False
|
||||||
CATEGORY = "autotrigger"
|
CATEGORY = "autotrigger"
|
||||||
@@ -96,127 +97,7 @@ class LoraLoaderStackedVanilla:
|
|||||||
if lora_stack is not None:
|
if lora_stack is not None:
|
||||||
loras.extend(lora_stack)
|
loras.extend(lora_stack)
|
||||||
|
|
||||||
return (civitai_tags_list, meta_tags_list, loras)
|
return (civitai_tags_list, meta_tags_list, loras, lora_name)
|
||||||
|
|
||||||
class LoraLoaderAdvanced:
|
|
||||||
def __init__(self):
|
|
||||||
self.loaded_lora = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(s):
|
|
||||||
LORA_LIST = sorted(folder_paths.get_filename_list("loras"), key=str.lower)
|
|
||||||
populate_items(LORA_LIST, "loras")
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"model": ("MODEL",),
|
|
||||||
"clip": ("CLIP", ),
|
|
||||||
"lora_name": (LORA_LIST, ),
|
|
||||||
"strength_model": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
|
||||||
"strength_clip": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
|
||||||
"force_fetch": ("BOOLEAN", {"default": False}),
|
|
||||||
"enable_preview": ("BOOLEAN", {"default": False}),
|
|
||||||
"append_loraname_if_empty": ("BOOLEAN", {"default": False}),
|
|
||||||
},
|
|
||||||
"optional": {
|
|
||||||
"override_lora_name":("STRING", {"forceInput": True}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("MODEL", "CLIP", "LIST", "LIST")
|
|
||||||
RETURN_NAMES = ("MODEL", "CLIP", "civitai_tags_list", "meta_tags_list")
|
|
||||||
FUNCTION = "load_lora"
|
|
||||||
CATEGORY = "autotrigger"
|
|
||||||
|
|
||||||
def load_lora(self, model, clip, lora_name, strength_model, strength_clip, force_fetch, enable_preview, append_loraname_if_empty, override_lora_name=""):
|
|
||||||
if override_lora_name != "":
|
|
||||||
lora_name = {"content": override_lora_name, "image": ""}
|
|
||||||
|
|
||||||
meta_tags_list = sort_tags_by_frequency(get_metadata(lora_name["content"], "loras"))
|
|
||||||
civitai_tags_list = load_and_save_tags(lora_name["content"], force_fetch)
|
|
||||||
|
|
||||||
civitai_tags_list = append_lora_name_if_empty(civitai_tags_list, lora_name["content"], append_loraname_if_empty)
|
|
||||||
meta_tags_list = append_lora_name_if_empty(meta_tags_list, lora_name["content"], append_loraname_if_empty)
|
|
||||||
|
|
||||||
lora_path = folder_paths.get_full_path("loras", lora_name["content"])
|
|
||||||
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 = load_torch_file(lora_path, safe_load=True)
|
|
||||||
self.loaded_lora = (lora_path, lora)
|
|
||||||
|
|
||||||
model_lora, clip_lora = load_lora_for_models(model, clip, lora, strength_model, strength_clip)
|
|
||||||
if enable_preview:
|
|
||||||
_, preview = copy_preview_to_temp(lora_name["image"])
|
|
||||||
if preview is not None:
|
|
||||||
preview_output = {
|
|
||||||
"filename": preview,
|
|
||||||
"subfolder": "lora_preview",
|
|
||||||
"type": "temp"
|
|
||||||
}
|
|
||||||
return {"ui": {"images": [preview_output]}, "result": (model_lora, clip_lora, civitai_tags_list, meta_tags_list)}
|
|
||||||
|
|
||||||
|
|
||||||
return (model_lora, clip_lora, civitai_tags_list, meta_tags_list)
|
|
||||||
|
|
||||||
class LoraLoaderStackedAdvanced:
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(s):
|
|
||||||
LORA_LIST = folder_paths.get_filename_list("loras")
|
|
||||||
populate_items(LORA_LIST, "loras")
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"lora_name": (LORA_LIST,),
|
|
||||||
"lora_weight": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
|
|
||||||
"force_fetch": ("BOOLEAN", {"default": False}),
|
|
||||||
"enable_preview": ("BOOLEAN", {"default": False}),
|
|
||||||
"append_loraname_if_empty": ("BOOLEAN", {"default": False}),
|
|
||||||
},
|
|
||||||
"optional": {
|
|
||||||
"lora_stack": ("LORA_STACK", ),
|
|
||||||
"override_lora_name":("STRING", {"forceInput": True}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("LIST", "LIST", "LORA_STACK",)
|
|
||||||
RETURN_NAMES = ("civitai_tags_list", "meta_tags_list", "LORA_STACK",)
|
|
||||||
FUNCTION = "set_stack"
|
|
||||||
#OUTPUT_NODE = False
|
|
||||||
CATEGORY = "autotrigger"
|
|
||||||
|
|
||||||
def set_stack(self, lora_name, lora_weight, force_fetch, enable_preview, append_loraname_if_empty, lora_stack=None, override_lora_name=""):
|
|
||||||
if override_lora_name != "":
|
|
||||||
lora_name = {"content": override_lora_name, "image": ""}
|
|
||||||
|
|
||||||
civitai_tags_list = load_and_save_tags(lora_name["content"], force_fetch)
|
|
||||||
|
|
||||||
meta_tags = get_metadata(lora_name["content"], "loras")
|
|
||||||
meta_tags_list = sort_tags_by_frequency(meta_tags)
|
|
||||||
|
|
||||||
civitai_tags_list = append_lora_name_if_empty(civitai_tags_list, lora_name["content"], append_loraname_if_empty)
|
|
||||||
meta_tags_list = append_lora_name_if_empty(meta_tags_list, lora_name["content"], append_loraname_if_empty)
|
|
||||||
|
|
||||||
loras = [(lora_name["content"],lora_weight,lora_weight,)]
|
|
||||||
if lora_stack is not None:
|
|
||||||
loras.extend(lora_stack)
|
|
||||||
|
|
||||||
if enable_preview:
|
|
||||||
_, preview = copy_preview_to_temp(lora_name["image"])
|
|
||||||
if preview is not None:
|
|
||||||
preview_output = {
|
|
||||||
"filename": preview,
|
|
||||||
"subfolder": "lora_preview",
|
|
||||||
"type": "temp"
|
|
||||||
}
|
|
||||||
return {"ui": {"images": [preview_output]}, "result": (civitai_tags_list, meta_tags_list, loras)}
|
|
||||||
|
|
||||||
return {"result": (civitai_tags_list, meta_tags_list, loras)}
|
|
||||||
|
|
||||||
class LoraTagsOnly:
|
class LoraTagsOnly:
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -249,14 +130,13 @@ class LoraTagsOnly:
|
|||||||
|
|
||||||
return (civitai_tags_list, meta_tags_list)
|
return (civitai_tags_list, meta_tags_list)
|
||||||
|
|
||||||
|
|
||||||
# A dictionary that contains all nodes you want to export with their names
|
# A dictionary that contains all nodes you want to export with their names
|
||||||
# NOTE: names should be globally unique
|
# NOTE: names should be globally unique
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"LoraLoaderVanilla": LoraLoaderVanilla,
|
"LoraLoaderVanilla": LoraLoaderVanilla,
|
||||||
"LoraLoaderStackedVanilla": LoraLoaderStackedVanilla,
|
"LoraLoaderStackedVanilla": LoraLoaderStackedVanilla,
|
||||||
"LoraLoaderAdvanced": LoraLoaderAdvanced,
|
"LoraLoaderAdvanced": LoraLoaderVanilla,
|
||||||
"LoraLoaderStackedAdvanced": LoraLoaderStackedAdvanced,
|
"LoraLoaderStackedAdvanced": LoraLoaderStackedVanilla,
|
||||||
"LoraTagsOnly": LoraTagsOnly,
|
"LoraTagsOnly": LoraTagsOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[project]
|
||||||
|
name = "comfyui-lora-auto-trigger-words"
|
||||||
|
description = "The aim of these custom nodes is to get an easy access to the tags used to trigger a lora / lycoris. Extract the tags from civitai or from the safetensors metadatas when available."
|
||||||
|
version = "1.1.0"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/idrirap/ComfyUI-Lora-Auto-Trigger-Words"
|
||||||
|
# Used by Comfy Registry https://comfyregistry.org
|
||||||
|
|
||||||
|
[tool.comfy]
|
||||||
|
PublisherId = "dijkstra"
|
||||||
|
DisplayName = "ComfyUI-Lora-Auto-Trigger-Words"
|
||||||
|
Icon = ""
|
||||||
@@ -3,58 +3,6 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
import shutil
|
|
||||||
|
|
||||||
def get_preview_path(name, type):
|
|
||||||
file_name = os.path.splitext(name)[0]
|
|
||||||
file_path = folder_paths.get_full_path(type, name)
|
|
||||||
|
|
||||||
if file_path is None:
|
|
||||||
print(f"Unable to get path for {type} {name}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
file_path_no_ext = os.path.splitext(file_path)[0]
|
|
||||||
item_image=None
|
|
||||||
for ext in ["png", "jpg", "jpeg", "preview.png"]:
|
|
||||||
has_image = os.path.isfile(file_path_no_ext + "." + ext)
|
|
||||||
if has_image:
|
|
||||||
item_image = f"{file_name}.{ext}"
|
|
||||||
break
|
|
||||||
|
|
||||||
return has_image, item_image
|
|
||||||
|
|
||||||
|
|
||||||
def copy_preview_to_temp(file_name):
|
|
||||||
if file_name is None:
|
|
||||||
return None, None
|
|
||||||
base_name = os.path.basename(file_name)
|
|
||||||
lora_less = "/".join(file_name.split("/")[1:])
|
|
||||||
|
|
||||||
file_path = folder_paths.get_full_path("loras", lora_less)
|
|
||||||
|
|
||||||
temp_path = folder_paths.get_temp_directory()
|
|
||||||
preview_path = os.path.join(temp_path, "lora_preview")
|
|
||||||
if not os.path.isdir(preview_path) :
|
|
||||||
os.makedirs(preview_path)
|
|
||||||
preview_path = os.path.join(preview_path, base_name)
|
|
||||||
|
|
||||||
|
|
||||||
shutil.copyfile(file_path, preview_path)
|
|
||||||
return preview_path, base_name
|
|
||||||
|
|
||||||
# add previews in selectors
|
|
||||||
def populate_items(names, type):
|
|
||||||
for idx, item_name in enumerate(names):
|
|
||||||
|
|
||||||
has_image, item_image = get_preview_path(item_name, type)
|
|
||||||
|
|
||||||
names[idx] = {
|
|
||||||
"content": item_name,
|
|
||||||
"image": f"{type}/{item_image}" if has_image else None,
|
|
||||||
"type": "loras",
|
|
||||||
}
|
|
||||||
names.sort(key=lambda i: i["content"].lower())
|
|
||||||
|
|
||||||
|
|
||||||
def load_json_from_file(file_path):
|
def load_json_from_file(file_path):
|
||||||
try:
|
try:
|
||||||
@@ -66,7 +14,7 @@ def load_json_from_file(file_path):
|
|||||||
return None
|
return None
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print(f"Error decoding JSON in file: {file_path}")
|
print(f"Error decoding JSON in file: {file_path}")
|
||||||
return None
|
raise
|
||||||
|
|
||||||
def save_dict_to_json(data_dict, file_path):
|
def save_dict_to_json(data_dict, file_path):
|
||||||
try:
|
try:
|
||||||
@@ -78,8 +26,11 @@ def save_dict_to_json(data_dict, file_path):
|
|||||||
|
|
||||||
def get_model_version_info(hash_value):
|
def get_model_version_info(hash_value):
|
||||||
api_url = f"https://civitai.com/api/v1/model-versions/by-hash/{hash_value}"
|
api_url = f"https://civitai.com/api/v1/model-versions/by-hash/{hash_value}"
|
||||||
response = requests.get(api_url)
|
try:
|
||||||
|
response = requests.get(api_url)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Lora-Auto-Trigger] {e}")
|
||||||
|
return None
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.json()
|
return response.json()
|
||||||
else:
|
else:
|
||||||
@@ -125,14 +76,6 @@ def load_and_save_tags(lora_name, force_fetch):
|
|||||||
|
|
||||||
return output_tags_list
|
return output_tags_list
|
||||||
|
|
||||||
def show_list(list_input):
|
|
||||||
i = 0
|
|
||||||
output = ""
|
|
||||||
for debug in list_input:
|
|
||||||
output += f"{i} : {debug}\n"
|
|
||||||
i+=1
|
|
||||||
return output
|
|
||||||
|
|
||||||
def get_metadata(filepath, type):
|
def get_metadata(filepath, type):
|
||||||
filepath = folder_paths.get_full_path(type, filepath)
|
filepath = folder_paths.get_full_path(type, filepath)
|
||||||
with open(filepath, "rb") as file:
|
with open(filepath, "rb") as file:
|
||||||
@@ -219,4 +162,4 @@ def append_lora_name_if_empty(tags_list, lora_path, enabled):
|
|||||||
filename = os.path.basename(filename)
|
filename = os.path.basename(filename)
|
||||||
|
|
||||||
tags_list.append(filename)
|
tags_list.append(filename)
|
||||||
return tags_list
|
return tags_list
|
||||||
|
|||||||
@@ -0,0 +1,469 @@
|
|||||||
|
// COMPLETELY STOLEN FROM https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/web/js/betterCombos.js
|
||||||
|
|
||||||
|
import { app } from "../../../scripts/app.js";
|
||||||
|
import { $el } from "../../../scripts/ui.js";
|
||||||
|
import { api } from "../../../scripts/api.js";
|
||||||
|
|
||||||
|
const LORA_LOADER = "LoraLoaderAdvanced"
|
||||||
|
const LORA_LOADER_STACKED = "LoraLoaderStackedAdvanced"
|
||||||
|
const IMAGE_WIDTH = 384;
|
||||||
|
const IMAGE_HEIGHT = 384;
|
||||||
|
|
||||||
|
function getType(node) {
|
||||||
|
return "loras";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWidgetName(type) {
|
||||||
|
return type === "checkpoints" ? "ckpt_name" : "lora_name";
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeRFC3986URIComponent(str) {
|
||||||
|
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateImagePosition = (el, bodyRect) => {
|
||||||
|
let { top, left, right } = el.getBoundingClientRect();
|
||||||
|
const { width: bodyWidth, height: bodyHeight } = bodyRect;
|
||||||
|
|
||||||
|
const isSpaceRight = right + IMAGE_WIDTH <= bodyWidth;
|
||||||
|
if (isSpaceRight) {
|
||||||
|
left = right;
|
||||||
|
} else {
|
||||||
|
left -= IMAGE_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
top = top - IMAGE_HEIGHT / 2;
|
||||||
|
if (top + IMAGE_HEIGHT > bodyHeight) {
|
||||||
|
top = bodyHeight - IMAGE_HEIGHT;
|
||||||
|
}
|
||||||
|
if (top < 0) {
|
||||||
|
top = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { left: Math.round(left), top: Math.round(top), isLeft: !isSpaceRight };
|
||||||
|
};
|
||||||
|
|
||||||
|
function showImage(relativeToEl, imageEl) {
|
||||||
|
const bodyRect = document.body.getBoundingClientRect();
|
||||||
|
if (!bodyRect) return;
|
||||||
|
|
||||||
|
const { left, top, isLeft } = calculateImagePosition(relativeToEl, bodyRect);
|
||||||
|
|
||||||
|
imageEl.style.left = `${left}px`;
|
||||||
|
imageEl.style.top = `${top}px`;
|
||||||
|
|
||||||
|
if (isLeft) {
|
||||||
|
imageEl.classList.add("left");
|
||||||
|
} else {
|
||||||
|
imageEl.classList.remove("left");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(imageEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
let imagesByType = {};
|
||||||
|
const loadImageList = async (type) => {
|
||||||
|
imagesByType[type] = await (await api.fetchApi(`/pysssss/images/${type}`)).json();
|
||||||
|
};
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "autotrigger.Combo++",
|
||||||
|
init() {
|
||||||
|
const displayOptions = { "List (normal)": 0, "Tree (subfolders)": 1, "Thumbnails (grid)": 2 };
|
||||||
|
const displaySetting = app.ui.settings.addSetting({
|
||||||
|
id: "autotrigger.Combo++.Submenu",
|
||||||
|
name: "🐍 Lora & Checkpoint loader display mode",
|
||||||
|
defaultValue: 1,
|
||||||
|
type: "combo",
|
||||||
|
options: (value) => {
|
||||||
|
value = +value;
|
||||||
|
|
||||||
|
return Object.entries(displayOptions).map(([k, v]) => ({
|
||||||
|
value: v,
|
||||||
|
text: k,
|
||||||
|
selected: k === value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$el("style", {
|
||||||
|
textContent: `
|
||||||
|
.pysssss-combo-image {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: ${IMAGE_WIDTH}px;
|
||||||
|
height: ${IMAGE_HEIGHT}px;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: top left;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.pysssss-combo-image.left {
|
||||||
|
object-position: top right;
|
||||||
|
}
|
||||||
|
.pysssss-combo-folder { opacity: 0.7 }
|
||||||
|
.pysssss-combo-folder-arrow { display: inline-block; width: 15px; }
|
||||||
|
.pysssss-combo-folder:hover { background-color: rgba(255, 255, 255, 0.1); }
|
||||||
|
.pysssss-combo-prefix { display: none }
|
||||||
|
|
||||||
|
/* Special handling for when the filter input is populated to revert to normal */
|
||||||
|
.litecontextmenu:has(input:not(:placeholder-shown)) .pysssss-combo-folder-contents {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
.litecontextmenu:has(input:not(:placeholder-shown)) .pysssss-combo-folder {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.litecontextmenu:has(input:not(:placeholder-shown)) .pysssss-combo-prefix {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.litecontextmenu:has(input:not(:placeholder-shown)) .litemenu-entry {
|
||||||
|
padding-left: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid mode */
|
||||||
|
.pysssss-combo-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-width: 60vw;
|
||||||
|
}
|
||||||
|
.pysssss-combo-grid .comfy-context-menu-filter {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.pysssss-combo-grid .litemenu-entry {
|
||||||
|
word-break: break-word;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.pysssss-combo-grid .litemenu-entry:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 250px;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
/* No-image image attribution: Picture icons created by Pixel perfect - Flaticon */
|
||||||
|
background-image: var(--background-image, url(extensions/ComfyUI-Custom-Scripts/js/assets/no-image.png));
|
||||||
|
}
|
||||||
|
|
||||||
|
`,
|
||||||
|
parent: document.body,
|
||||||
|
});
|
||||||
|
const p1 = loadImageList("checkpoints");
|
||||||
|
const p2 = loadImageList("loras");
|
||||||
|
|
||||||
|
const refreshComboInNodes = app.refreshComboInNodes;
|
||||||
|
app.refreshComboInNodes = async function () {
|
||||||
|
const r = await Promise.all([
|
||||||
|
refreshComboInNodes.apply(this, arguments),
|
||||||
|
loadImageList("checkpoints").catch(() => {}),
|
||||||
|
loadImageList("loras").catch(() => {}),
|
||||||
|
]);
|
||||||
|
return r[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageHost = $el("img.pysssss-combo-image");
|
||||||
|
|
||||||
|
const positionMenu = (menu, fillWidth) => {
|
||||||
|
// compute best position
|
||||||
|
let left = app.canvas.last_mouse[0] - 10;
|
||||||
|
let top = app.canvas.last_mouse[1] - 10;
|
||||||
|
|
||||||
|
const body_rect = document.body.getBoundingClientRect();
|
||||||
|
const root_rect = menu.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (body_rect.width && left > body_rect.width - root_rect.width - 10) left = body_rect.width - root_rect.width - 10;
|
||||||
|
if (body_rect.height && top > body_rect.height - root_rect.height - 10) top = body_rect.height - root_rect.height - 10;
|
||||||
|
|
||||||
|
menu.style.left = `${left}px`;
|
||||||
|
menu.style.top = `${top}px`;
|
||||||
|
if (fillWidth) {
|
||||||
|
menu.style.right = "10px";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMenu = async (menu, type) => {
|
||||||
|
try {
|
||||||
|
await p1;
|
||||||
|
await p2;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
console.error("Error loading pysssss.betterCombos data");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp max height so it doesn't overflow the screen
|
||||||
|
const position = menu.getBoundingClientRect();
|
||||||
|
const maxHeight = window.innerHeight - position.top - 20;
|
||||||
|
menu.style.maxHeight = `${maxHeight}px`;
|
||||||
|
|
||||||
|
const images = imagesByType[type];
|
||||||
|
const items = menu.querySelectorAll(".litemenu-entry");
|
||||||
|
|
||||||
|
// Add image handler to items
|
||||||
|
const addImageHandler = (item) => {
|
||||||
|
const text = item.getAttribute("data-value").trim();
|
||||||
|
if (images[text]) {
|
||||||
|
const textNode = document.createTextNode("*");
|
||||||
|
item.appendChild(textNode);
|
||||||
|
|
||||||
|
item.addEventListener(
|
||||||
|
"mouseover",
|
||||||
|
() => {
|
||||||
|
imageHost.src = `/pysssss/view/${encodeRFC3986URIComponent(images[text])}?${+new Date()}`;
|
||||||
|
document.body.appendChild(imageHost);
|
||||||
|
showImage(item, imageHost);
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
item.addEventListener(
|
||||||
|
"mouseout",
|
||||||
|
() => {
|
||||||
|
imageHost.remove();
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
item.addEventListener(
|
||||||
|
"click",
|
||||||
|
() => {
|
||||||
|
imageHost.remove();
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTree = () => {
|
||||||
|
// Create a map to store folder structures
|
||||||
|
const folderMap = new Map();
|
||||||
|
const rootItems = [];
|
||||||
|
const splitBy = (navigator.platform || navigator.userAgent).includes("Win") ? /\/|\\/ : /\//;
|
||||||
|
const itemsSymbol = Symbol("items");
|
||||||
|
|
||||||
|
// First pass - organize items into folder structure
|
||||||
|
for (const item of items) {
|
||||||
|
const path = item.getAttribute("data-value").split(splitBy);
|
||||||
|
|
||||||
|
// Remove path from visible text
|
||||||
|
item.textContent = path[path.length - 1];
|
||||||
|
if (path.length > 1) {
|
||||||
|
// Add the prefix path back in so it can be filtered on
|
||||||
|
const prefix = $el("span.pysssss-combo-prefix", {
|
||||||
|
textContent: path.slice(0, -1).join("/") + "/",
|
||||||
|
});
|
||||||
|
item.prepend(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
addImageHandler(item);
|
||||||
|
|
||||||
|
if (path.length === 1) {
|
||||||
|
rootItems.push(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporarily remove the item from current position
|
||||||
|
item.remove();
|
||||||
|
|
||||||
|
// Create folder hierarchy
|
||||||
|
let currentLevel = folderMap;
|
||||||
|
for (let i = 0; i < path.length - 1; i++) {
|
||||||
|
const folder = path[i];
|
||||||
|
if (!currentLevel.has(folder)) {
|
||||||
|
currentLevel.set(folder, new Map());
|
||||||
|
}
|
||||||
|
currentLevel = currentLevel.get(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the actual item in the deepest folder
|
||||||
|
if (!currentLevel.has(itemsSymbol)) {
|
||||||
|
currentLevel.set(itemsSymbol, []);
|
||||||
|
}
|
||||||
|
currentLevel.get(itemsSymbol).push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFolderElement = (name) => {
|
||||||
|
const folder = $el("div.litemenu-entry.pysssss-combo-folder", {
|
||||||
|
innerHTML: `<span class="pysssss-combo-folder-arrow">▶</span> ${name}`,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
return folder;
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertFolderStructure = (parentElement, map, level = 0) => {
|
||||||
|
for (const [folderName, content] of map.entries()) {
|
||||||
|
if (folderName === itemsSymbol) continue;
|
||||||
|
|
||||||
|
const folderElement = createFolderElement(folderName);
|
||||||
|
folderElement.style.paddingLeft = `${level * 10 + 5}px`;
|
||||||
|
parentElement.appendChild(folderElement);
|
||||||
|
|
||||||
|
const childContainer = $el("div.pysssss-combo-folder-contents", {
|
||||||
|
style: { display: "none" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add items in this folder
|
||||||
|
const items = content.get(itemsSymbol) || [];
|
||||||
|
for (const item of items) {
|
||||||
|
item.style.paddingLeft = `${(level + 1) * 10 + 14}px`;
|
||||||
|
childContainer.appendChild(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively add subfolders
|
||||||
|
insertFolderStructure(childContainer, content, level + 1);
|
||||||
|
parentElement.appendChild(childContainer);
|
||||||
|
|
||||||
|
// Add click handler for folder
|
||||||
|
folderElement.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const arrow = folderElement.querySelector(".pysssss-combo-folder-arrow");
|
||||||
|
const contents = folderElement.nextElementSibling;
|
||||||
|
if (contents.style.display === "none") {
|
||||||
|
contents.style.display = "block";
|
||||||
|
arrow.textContent = "▼";
|
||||||
|
} else {
|
||||||
|
contents.style.display = "none";
|
||||||
|
arrow.textContent = "▶";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
insertFolderStructure(items[0]?.parentElement || menu, folderMap);
|
||||||
|
positionMenu(menu);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addImageData = (item) => {
|
||||||
|
const text = item.getAttribute("data-value").trim();
|
||||||
|
if (images[text]) {
|
||||||
|
item.style.setProperty("--background-image", `url(/pysssss/view/${encodeRFC3986URIComponent(images[text])})`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (displaySetting.value === 1 || displaySetting.value === true) {
|
||||||
|
createTree();
|
||||||
|
} else if (displaySetting.value === 2) {
|
||||||
|
menu.classList.add("pysssss-combo-grid");
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
addImageData(item);
|
||||||
|
}
|
||||||
|
positionMenu(menu, true);
|
||||||
|
} else {
|
||||||
|
for (const item of items) {
|
||||||
|
addImageHandler(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutationObserver = new MutationObserver((mutations) => {
|
||||||
|
const node = app.canvas.current_node;
|
||||||
|
|
||||||
|
if (!node || (node.comfyClass !== LORA_LOADER && node.comfyClass !== LORA_LOADER_STACKED)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
for (const removed of mutation.removedNodes) {
|
||||||
|
if (removed.classList?.contains("litecontextmenu")) {
|
||||||
|
imageHost.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const added of mutation.addedNodes) {
|
||||||
|
if (added.classList?.contains("litecontextmenu")) {
|
||||||
|
const overWidget = app.canvas.getWidgetAtCursor();
|
||||||
|
const type = getType(node);
|
||||||
|
if (overWidget?.name === getWidgetName(type)) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// Bad hack to prevent showing on right click menu by checking for the filter input
|
||||||
|
if (!added.querySelector(".comfy-context-menu-filter")) return;
|
||||||
|
updateMenu(added, type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mutationObserver.observe(document.body, { childList: true, subtree: false });
|
||||||
|
},
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
|
const isLora = (nodeData.name === LORA_LOADER || nodeData.name === LORA_LOADER_STACKED);
|
||||||
|
if (isLora) {
|
||||||
|
const onAdded = nodeType.prototype.onAdded;
|
||||||
|
nodeType.prototype.onAdded = function () {
|
||||||
|
onAdded?.apply(this, arguments);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const modelWidget = this.widgets[0];
|
||||||
|
const modelCb = modelWidget.callback;
|
||||||
|
let prev = undefined;
|
||||||
|
modelWidget.callback = function () {
|
||||||
|
let ret = modelCb?.apply(this, arguments) ?? modelWidget.value;
|
||||||
|
if (typeof ret === "object" && "content" in ret) {
|
||||||
|
ret = ret.content;
|
||||||
|
modelWidget.value = ret;
|
||||||
|
}
|
||||||
|
let v = ret;
|
||||||
|
if (prev !== v) {
|
||||||
|
prev = v;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
setTimeout(() => {
|
||||||
|
modelWidget.callback();
|
||||||
|
}, 30);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||||
|
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||||
|
if (this.imgs) {
|
||||||
|
// If this node has images then we add an open in new tab item
|
||||||
|
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) {
|
||||||
|
const nodes = app.graph._nodes.filter((n) => n.comfyClass === LORA_LOADER || n.comfyClass === LORA_LOADER_STACKED);
|
||||||
|
if (nodes.length) {
|
||||||
|
options.unshift({
|
||||||
|
content: "Save as Preview",
|
||||||
|
submenu: {
|
||||||
|
options: nodes.map((n) => ({
|
||||||
|
content: n.widgets[0].value,
|
||||||
|
callback: async () => {
|
||||||
|
const url = new URL(img.src);
|
||||||
|
await api.fetchApi("/pysssss/save/" + encodeRFC3986URIComponent(`${getType(n)}/${n.widgets[0].value}`), {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
filename: url.searchParams.get("filename"),
|
||||||
|
subfolder: url.searchParams.get("subfolder"),
|
||||||
|
type: url.searchParams.get("type"),
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
loadImageList(getType(n));
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getExtraMenuOptions?.apply(this, arguments);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||