mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 08:26:45 -03:00
447 lines
12 KiB
JavaScript
447 lines
12 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
import { CONVERTED_TYPE, getNodeFromGraph } from "./utils.js";
|
|
import { addTagsWidget } from "./tags_widget.js";
|
|
import { getWheelSensitivity } from "./settings.js";
|
|
|
|
function normalizeTagText(text) {
|
|
return typeof text === "string" ? text.trim().toLowerCase() : "";
|
|
}
|
|
|
|
function splitTopLevelCommas(text) {
|
|
if (typeof text !== "string" || !text.trim()) {
|
|
return [];
|
|
}
|
|
|
|
const parts = [];
|
|
let current = "";
|
|
let depth = 0;
|
|
|
|
for (const char of text) {
|
|
if (char === "(") {
|
|
depth += 1;
|
|
current += char;
|
|
continue;
|
|
}
|
|
if (char === ")") {
|
|
depth = Math.max(0, depth - 1);
|
|
current += char;
|
|
continue;
|
|
}
|
|
if (char === "," && depth === 0) {
|
|
const trimmed = current.trim();
|
|
if (trimmed) {
|
|
parts.push(trimmed);
|
|
}
|
|
current = "";
|
|
continue;
|
|
}
|
|
current += char;
|
|
}
|
|
|
|
const trimmed = current.trim();
|
|
if (trimmed) {
|
|
parts.push(trimmed);
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
function isGroupTag(tag) {
|
|
return Array.isArray(tag?.items);
|
|
}
|
|
|
|
function parseSerializedText(text) {
|
|
const normalizedText = typeof text === "string" ? text.trim() : "";
|
|
const strengthMatch = normalizedText.match(/^\((.+):([\d.]+)\)$/);
|
|
if (!strengthMatch) {
|
|
return {
|
|
text: normalizedText,
|
|
strength: null,
|
|
};
|
|
}
|
|
|
|
const parsedStrength = Number(strengthMatch[2]);
|
|
return {
|
|
text: strengthMatch[1].trim(),
|
|
strength: Number.isFinite(parsedStrength) ? parsedStrength : null,
|
|
};
|
|
}
|
|
|
|
function cloneTag(tag) {
|
|
if (!isGroupTag(tag)) {
|
|
return { ...tag };
|
|
}
|
|
return {
|
|
...tag,
|
|
items: tag.items.map((item) => ({ ...item })),
|
|
};
|
|
}
|
|
|
|
function collectHighlightTokens(wordsArray) {
|
|
const tokens = new Set();
|
|
|
|
const addToken = (text) => {
|
|
const normalized = normalizeTagText(text);
|
|
if (normalized) {
|
|
tokens.add(normalized);
|
|
}
|
|
};
|
|
|
|
wordsArray.forEach((rawWord) => {
|
|
if (typeof rawWord !== "string") {
|
|
return;
|
|
}
|
|
|
|
addToken(rawWord);
|
|
|
|
const groupParts = rawWord.split(/,{2,}/);
|
|
groupParts.forEach((groupPart) => {
|
|
addToken(groupPart);
|
|
splitTopLevelCommas(groupPart).forEach(addToken);
|
|
});
|
|
|
|
splitTopLevelCommas(rawWord).forEach(addToken);
|
|
});
|
|
|
|
return tokens;
|
|
}
|
|
|
|
function buildLegacyTagState(existingTags, allowStrengthAdjustment) {
|
|
return existingTags.reduce((acc, tag) => {
|
|
const parsed = parseSerializedText(tag.text);
|
|
const key = parsed.text;
|
|
if (!acc[key]) {
|
|
acc[key] = [];
|
|
}
|
|
acc[key].push({
|
|
active: tag.active,
|
|
strength:
|
|
allowStrengthAdjustment
|
|
? (tag.strength !== undefined && tag.strength !== null ? tag.strength : parsed.strength)
|
|
: null,
|
|
});
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
function buildGroupState(existingTags, allowStrengthAdjustment) {
|
|
return existingTags.reduce((acc, tag) => {
|
|
const parsed = parseSerializedText(tag.text);
|
|
const key = parsed.text;
|
|
if (!acc[key]) {
|
|
acc[key] = [];
|
|
}
|
|
|
|
const itemState = {};
|
|
if (Array.isArray(tag.items)) {
|
|
tag.items.forEach((item) => {
|
|
const itemKey = item.text;
|
|
if (!itemState[itemKey]) {
|
|
itemState[itemKey] = [];
|
|
}
|
|
itemState[itemKey].push({
|
|
active: item.active,
|
|
});
|
|
});
|
|
} else {
|
|
splitTopLevelCommas(tag.text).forEach((itemText) => {
|
|
if (!itemState[itemText]) {
|
|
itemState[itemText] = [];
|
|
}
|
|
itemState[itemText].push({
|
|
active: tag.active,
|
|
});
|
|
});
|
|
}
|
|
|
|
acc[key].push({
|
|
active: tag.active,
|
|
strength:
|
|
allowStrengthAdjustment
|
|
? (tag.strength !== undefined && tag.strength !== null ? tag.strength : parsed.strength)
|
|
: null,
|
|
itemState,
|
|
});
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
function consumeQueuedState(stateMap, key) {
|
|
const queue = stateMap[key];
|
|
if (queue && queue.length > 0) {
|
|
return queue.shift();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "LoraManager.TriggerWordToggle",
|
|
|
|
setup() {
|
|
api.addEventListener("trigger_word_update", (event) => {
|
|
const { id, graph_id: graphId, message } = event.detail;
|
|
this.handleTriggerWordUpdate(id, graphId, message);
|
|
});
|
|
},
|
|
|
|
async nodeCreated(node) {
|
|
if (node.comfyClass !== "TriggerWord Toggle (LoraManager)") {
|
|
return;
|
|
}
|
|
|
|
node.serialize_widgets = true;
|
|
node.addInput("trigger_words", "string", {
|
|
shape: 7,
|
|
});
|
|
|
|
requestAnimationFrame(async () => {
|
|
const wheelSensitivity = getWheelSensitivity();
|
|
const groupModeWidget = node.widgets[0];
|
|
const defaultActiveWidget = node.widgets[1];
|
|
const strengthAdjustmentWidget = node.widgets[2];
|
|
const initialStrengthAdjustment = Boolean(strengthAdjustmentWidget?.value);
|
|
|
|
const result = addTagsWidget(node, "toggle_trigger_words", {
|
|
defaultVal: [],
|
|
}, null, wheelSensitivity, {
|
|
allowStrengthAdjustment: initialStrengthAdjustment,
|
|
});
|
|
|
|
node.tagWidget = result.widget;
|
|
node.tagWidget.allowStrengthAdjustment = initialStrengthAdjustment;
|
|
|
|
const applyHighlightState = () => {
|
|
if (!node.tagWidget) {
|
|
return;
|
|
}
|
|
|
|
const highlightSet = node._highlightedTriggerWords || new Set();
|
|
const updatedTags = (node.tagWidget.value || []).map((tag) => {
|
|
if (Array.isArray(tag.items)) {
|
|
const items = tag.items.map((item) => ({
|
|
...item,
|
|
highlighted: highlightSet.size > 0 && highlightSet.has(normalizeTagText(item.text)),
|
|
}));
|
|
|
|
return {
|
|
...tag,
|
|
items,
|
|
highlighted:
|
|
highlightSet.size > 0 &&
|
|
(highlightSet.has(normalizeTagText(tag.text)) ||
|
|
items.some((item) => item.highlighted)),
|
|
};
|
|
}
|
|
|
|
return {
|
|
...tag,
|
|
highlighted: highlightSet.size > 0 && highlightSet.has(normalizeTagText(tag.text)),
|
|
};
|
|
});
|
|
|
|
node.tagWidget.value = updatedTags;
|
|
};
|
|
|
|
node.highlightTriggerWords = (triggerWords) => {
|
|
const wordsArray = Array.isArray(triggerWords)
|
|
? triggerWords
|
|
: triggerWords
|
|
? [triggerWords]
|
|
: [];
|
|
node._highlightedTriggerWords = collectHighlightTokens(wordsArray);
|
|
applyHighlightState();
|
|
};
|
|
|
|
if (node.__pendingHighlightWords !== undefined) {
|
|
const pending = node.__pendingHighlightWords;
|
|
delete node.__pendingHighlightWords;
|
|
node.highlightTriggerWords(pending);
|
|
}
|
|
|
|
node.applyTriggerHighlightState = applyHighlightState;
|
|
|
|
const hiddenWidget = node.addWidget("text", "orinalMessage", "");
|
|
hiddenWidget.type = CONVERTED_TYPE;
|
|
hiddenWidget.hidden = true;
|
|
hiddenWidget.computeSize = () => [0, -4];
|
|
node.originalMessageWidget = hiddenWidget;
|
|
|
|
const tagWidgetIndex = node.widgets.indexOf(result.widget);
|
|
const originalMessageWidgetIndex = node.widgets.indexOf(hiddenWidget);
|
|
if (node.widgets_values && node.widgets_values.length > 0) {
|
|
if (tagWidgetIndex >= 0) {
|
|
const savedValue = node.widgets_values[tagWidgetIndex];
|
|
if (savedValue) {
|
|
result.widget.value = Array.isArray(savedValue) ? savedValue : [];
|
|
}
|
|
}
|
|
if (originalMessageWidgetIndex >= 0) {
|
|
const originalMessage = node.widgets_values[originalMessageWidgetIndex];
|
|
if (originalMessage) {
|
|
hiddenWidget.value = originalMessage;
|
|
}
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(() => node.applyTriggerHighlightState?.());
|
|
|
|
groupModeWidget.callback = (value) => {
|
|
node.tagWidget?.closeGroupEditor?.();
|
|
if (node.originalMessageWidget?.value) {
|
|
this.updateTagsBasedOnMode(
|
|
node,
|
|
node.originalMessageWidget.value,
|
|
value,
|
|
Boolean(strengthAdjustmentWidget?.value)
|
|
);
|
|
}
|
|
};
|
|
|
|
defaultActiveWidget.callback = (value) => {
|
|
if (!node.tagWidget || !node.tagWidget.value) {
|
|
return;
|
|
}
|
|
|
|
const updatedTags = node.tagWidget.value.map((tag) => {
|
|
if (!Array.isArray(tag.items)) {
|
|
return {
|
|
...tag,
|
|
active: value,
|
|
};
|
|
}
|
|
|
|
return {
|
|
...tag,
|
|
active: value,
|
|
items: tag.items.map((item) => ({
|
|
...item,
|
|
active: value,
|
|
})),
|
|
};
|
|
});
|
|
|
|
node.tagWidget.value = updatedTags;
|
|
node.applyTriggerHighlightState?.();
|
|
};
|
|
|
|
if (strengthAdjustmentWidget) {
|
|
strengthAdjustmentWidget.callback = (value) => {
|
|
const allowStrengthAdjustment = Boolean(value);
|
|
if (node.tagWidget) {
|
|
node.tagWidget.allowStrengthAdjustment = allowStrengthAdjustment;
|
|
node.tagWidget.closeGroupEditor?.();
|
|
}
|
|
this.updateTagsBasedOnMode(
|
|
node,
|
|
node.originalMessageWidget?.value || "",
|
|
groupModeWidget?.value ?? false,
|
|
allowStrengthAdjustment
|
|
);
|
|
};
|
|
}
|
|
|
|
result.widget.serializeValue = function() {
|
|
const value = this.value || [];
|
|
return value.map((tag) => {
|
|
if (Array.isArray(tag.items)) {
|
|
return {
|
|
...tag,
|
|
text:
|
|
tag.strength !== undefined && tag.strength !== null
|
|
? `(${tag.text}:${tag.strength.toFixed(2)})`
|
|
: tag.text,
|
|
items: tag.items.map((item) => ({ ...item })),
|
|
};
|
|
}
|
|
|
|
if (tag.strength !== undefined && tag.strength !== null) {
|
|
return {
|
|
...tag,
|
|
text: `(${tag.text}:${tag.strength.toFixed(2)})`,
|
|
};
|
|
}
|
|
|
|
return tag;
|
|
});
|
|
};
|
|
});
|
|
},
|
|
|
|
handleTriggerWordUpdate(id, graphId, message) {
|
|
const node = getNodeFromGraph(graphId, id);
|
|
if (!node || node.comfyClass !== "TriggerWord Toggle (LoraManager)") {
|
|
console.warn("Node not found or not a TriggerWordToggle:", id);
|
|
return;
|
|
}
|
|
|
|
if (node.originalMessageWidget) {
|
|
node.originalMessageWidget.value = message;
|
|
}
|
|
|
|
if (node.tagWidget) {
|
|
const groupMode = node.widgets[0] ? node.widgets[0].value : false;
|
|
const allowStrengthAdjustment = Boolean(node.widgets[2]?.value);
|
|
node.tagWidget.allowStrengthAdjustment = allowStrengthAdjustment;
|
|
this.updateTagsBasedOnMode(node, message, groupMode, allowStrengthAdjustment);
|
|
}
|
|
},
|
|
|
|
updateTagsBasedOnMode(node, message, groupMode, allowStrengthAdjustment = false) {
|
|
if (!node.tagWidget) {
|
|
return;
|
|
}
|
|
node.tagWidget.closeGroupEditor?.();
|
|
node.tagWidget.allowStrengthAdjustment = allowStrengthAdjustment;
|
|
|
|
const existingTags = (node.tagWidget.value || []).map(cloneTag);
|
|
const defaultActive = node.widgets[1] ? node.widgets[1].value : true;
|
|
let tagArray = [];
|
|
|
|
if (groupMode) {
|
|
const existingGroupState = buildGroupState(existingTags, allowStrengthAdjustment);
|
|
const groups = message.trim()
|
|
? (message.includes(",,") ? message.split(/,{2,}/) : [message])
|
|
.map((group) => group.trim())
|
|
.filter(Boolean)
|
|
: [];
|
|
|
|
tagArray = groups.map((group) => {
|
|
const existing = consumeQueuedState(existingGroupState, group);
|
|
const itemState = existing?.itemState || {};
|
|
const items = splitTopLevelCommas(group).map((itemText) => {
|
|
const savedItem = consumeQueuedState(itemState, itemText);
|
|
return {
|
|
text: itemText,
|
|
active: savedItem ? savedItem.active : true,
|
|
highlighted: false,
|
|
strength: null,
|
|
};
|
|
});
|
|
|
|
return {
|
|
text: group,
|
|
active: existing ? existing.active : defaultActive,
|
|
highlighted: false,
|
|
strength: existing ? existing.strength : null,
|
|
items,
|
|
};
|
|
});
|
|
} else {
|
|
const existingTagState = buildLegacyTagState(existingTags, allowStrengthAdjustment);
|
|
tagArray = splitTopLevelCommas(message).map((word) => {
|
|
const existing = consumeQueuedState(existingTagState, word);
|
|
return {
|
|
text: word,
|
|
active: existing ? existing.active : defaultActive,
|
|
highlighted: false,
|
|
strength: existing ? existing.strength : null,
|
|
};
|
|
});
|
|
}
|
|
|
|
node.tagWidget.value = tagArray;
|
|
node.applyTriggerHighlightState?.();
|
|
},
|
|
});
|