Merge pull request #52 from idrirap/dev/fix_advanced_nodes

fix advanced nodes
This commit is contained in:
Dijkstra
2025-07-26 19:43:22 +02:00
committed by GitHub
6 changed files with 481 additions and 233 deletions

View File

@@ -98,8 +98,15 @@ The format is simple. It's the same as python list index, but can select multipl
### 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
![image](./images/loaderAdvanced.png)

View File

@@ -57,7 +57,6 @@ class LoraLoaderVanilla:
self.loaded_lora = (lora_path, lora)
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)
class LoraLoaderStackedVanilla:
@@ -100,132 +99,6 @@ class LoraLoaderStackedVanilla:
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",),
"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": {
"clip": ("CLIP", ),
"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, lora_name, strength_model, strength_clip, force_fetch, enable_preview, append_loraname_if_empty, clip=None, override_lora_name=""):
if clip is None:
strength_clip=0
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:
@classmethod
def INPUT_TYPES(s):
@@ -262,8 +135,8 @@ class LoraTagsOnly:
NODE_CLASS_MAPPINGS = {
"LoraLoaderVanilla": LoraLoaderVanilla,
"LoraLoaderStackedVanilla": LoraLoaderStackedVanilla,
"LoraLoaderAdvanced": LoraLoaderAdvanced,
"LoraLoaderStackedAdvanced": LoraLoaderStackedAdvanced,
"LoraLoaderAdvanced": LoraLoaderVanilla,
"LoraLoaderStackedAdvanced": LoraLoaderStackedVanilla,
"LoraTagsOnly": LoraTagsOnly,
}

View File

@@ -1,7 +1,7 @@
[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.0.4"
version = "1.1.0"
license = "MIT"
[project.urls]

View File

@@ -3,63 +3,6 @@ import hashlib
import json
import os
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", "gif"]:
if item_image is not None:
break
for ext2 in ["", ".preview"]:
has_image = os.path.isfile(file_path_no_ext + ext2 + "." + ext)
if has_image:
item_image = f"{file_name}{ext2}.{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):
try:
@@ -133,14 +76,6 @@ def load_and_save_tags(lora_name, force_fetch):
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):
filepath = folder_paths.get_full_path(type, filepath)
with open(filepath, "rb") as file:

469
web/js/betterCombos.js Normal file
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);
};
},
});

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);
};
}
});