Compare commits

..
Author SHA1 Message Date
Dijkstra f752500e74 chore: forgot to update the version 2025-07-26 19:39:42 +02:00
Dijkstra 11af9469ee fix: removed all the advanced node stuff. Replaced it with vanilla node with extra js took from custom-node repo. And added explanation for view info in the readme 2025-07-26 19:39:20 +02:00
Dijkstra abb9b39448 Merge pull request #51 from idrirap/dev/clear_readme
chore: detail the difference between tags and trigger words
2025-07-26 17:48:51 +02:00
Dijkstra 2e889c7018 chore: detail the difference between tags and trigger words 2025-07-26 17:46:52 +02:00
Dijkstra 52157dff5a Merge pull request #39 from idrirap/dev/issue38
fix: no longer crash when internet is unavailable
2025-01-16 09:38:20 +01:00
Dijkstra 8c18075e9f chore: update node version 2025-01-16 09:37:53 +01:00
Dijkstra e0ec5dee30 fix: no longer crash when internet is unavailable 2025-01-16 09:35:17 +01:00
Dijkstra c09e56b21f Merge pull request #36 from idrirap/dev/issue33
Dev/issue33
2024-10-31 19:53:14 +01:00
Dijkstra 763152c041 chore: set clip strength to 0 when there is no CLIP. (just in case) 2024-10-31 19:51:46 +01:00
Dijkstra c85b302a61 chore: CLIP is not optional in the LoraLoaderVanilla and LoraLoaderAdvanced 2024-10-31 19:49:13 +01:00
Dijkstra 6484968fbe Merge pull request #35 from idrirap/dev/issue34
fix: sometimes the preview image extension is prefixed with '.preview'
2024-10-31 19:15:58 +01:00
Dijkstra 9a212d13e7 fix: sometimes the preview image extension is prefixed with '.preview' 2024-10-31 19:14:31 +01:00
Dijkstra b02bea5480 chore: bump version + added MIT license 2024-06-20 17:56:49 +02:00
Dijkstra 8c4b7941a6 chore: improved description 2024-06-20 17:50:07 +02:00
dijkstra 070751dbf1 Merge pull request #24 from idrirap/dev/add_registryId
feat: set publisherId
2024-06-20 17:35:06 +02:00
Dijkstra 41f6277830 feat: set publisherId 2024-06-20 17:34:27 +02:00
dijkstra 03ddfbfe66 Merge pull request #21 from haohaocreates/publish
Add Github Action for Publishing to Comfy Registry
2024-06-20 17:28:55 +02:00
dijkstra 885a1c8371 Merge pull request #22 from haohaocreates/pyproject
Add pyproject.toml for Custom Node Registry
2024-06-20 17:28:45 +02:00
haohaocreates 8656b2ebaf chore(pyproject): Add pyproject.toml for Custom Node Registry 2024-05-22 17:25:44 -04:00
haohaocreates c3759e7b30 chore(publish): Add Github Action for Publishing to Comfy Registry 2024-05-22 17:25:41 -04:00
Dijkstra 236e230515 doc: added trigger explanation 2024-04-09 22:35:48 +02:00
dijkstra 23ef04b1cb Merge pull request #20 from idrirap/update_readme
Update readme
2024-04-09 22:18:38 +02:00
Dijkstra a95cba4e3b doc: last fix 2024-04-09 22:18:14 +02:00
Dijkstra 40f09582e2 doc: added View info in readme 2024-04-09 22:17:08 +02:00
Dijkstra 6635d30566 doc: updated readme 2024-04-09 22:11:55 +02:00
dijkstra 5cf49c9565 Merge pull request #19 from idrirap/issue14
feat: added pyssss's lora view info
2024-04-09 21:24:05 +02:00
15 changed files with 603 additions and 252 deletions
+21
View File
@@ -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 }}
+88 -22
View File
@@ -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)
![image](./images/CivitaiTriggerWords.png)
- 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.
![image](./images/main.png) ![image](./images/main.png)
#### Nodes ### Nodes
- LoraLoader (Vanilla or Advanced) #### LoraLoader (Vanilla and Advanced)
- LoraLoaderStacked (Vanilla or Avanced). The stacked lora input is optional. ![image](./images/LoraLoader.png)
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).
![image](./images/LoraLoaderStacked.png)
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
![image](./images/LoraTagsOnly.png)
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
![image](./images/TagsFormater.png)
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
![image](./images/TagsSelector.png)
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
![image](./images/LoraListNames.png)
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
![image](./images/ViewInfo.png)
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
![image](./images/loaderAdvanced.png) ![image](./images/loaderAdvanced.png)
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+6 -130
View File
@@ -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,6 +21,7 @@ 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}),
} }
} }
@@ -31,7 +31,9 @@ class LoraLoaderVanilla:
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,7 +57,6 @@ 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, lora_name)
class LoraLoaderStackedVanilla: class LoraLoaderStackedVanilla:
@@ -98,130 +99,6 @@ class LoraLoaderStackedVanilla:
return (civitai_tags_list, meta_tags_list, loras, lora_name) 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", "STRING")
RETURN_NAMES = ("MODEL", "CLIP", "civitai_tags_list", "meta_tags_list", "lora_name")
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 != "":
has_preview, prev = get_preview_path(override_lora_name, "loras")
prev = f"loras/{prev}" if has_preview else None
lora_name = {"content": override_lora_name, "image": prev, "type": "loras"}
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, lora_name["content"])}
return (model_lora, clip_lora, civitai_tags_list, meta_tags_list, lora_name["content"])
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", "STRING")
RETURN_NAMES = ("civitai_tags_list", "meta_tags_list", "LORA_STACK", "lora_name")
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 != "":
has_preview, prev = get_preview_path(override_lora_name, "loras")
prev = f"loras/{prev}" if has_preview else None
lora_name = {"content": override_lora_name, "image": prev, "type": "loras"}
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, lora_name["content"])}
return {"result": (civitai_tags_list, meta_tags_list, loras, lora_name["content"])}
class LoraTagsOnly: class LoraTagsOnly:
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
@@ -253,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,
} }
+14
View File
@@ -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 = ""
+5 -64
View File
@@ -3,60 +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)
if file_path is None:
return None, None
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:
@@ -80,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:
@@ -127,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:
+469
View File
@@ -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);
};
},
});
-36
View File
@@ -1,36 +0,0 @@
import { app } from "../../../scripts/app.js";
import { LoraInfoDialog } from "../../ComfyUI-Custom-Scripts/js/modelInfo.js";
const infoHandlers = {
"LoraLoaderVanilla":true,
"LoraLoaderStackedVanilla":true,
"LoraLoaderAdvanced":true,
"LoraLoaderStackedAdvanced":true
}
app.registerExtension({
name: "autotrigger.LoraInfo",
beforeRegisterNodeDef(nodeType) {
if (! infoHandlers[nodeType.comfyClass]) {
return;
}
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (_, options) {
let value = this.widgets[0].value;
if (!value) {
return;
}
if (value.content) {
value = value.content;
}
options.unshift({
content: "View info...",
callback: async () => {
new LoraInfoDialog(value).show("loras", value);
},
});
return getExtraMenuOptions?.apply(this, arguments);
};
}
});