mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 16:36:45 -03:00
Compare commits
2 Commits
1eeba666f5
...
79dd9a1b29
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79dd9a1b29 | ||
|
|
ef4923fd94 |
21
py/config.py
21
py/config.py
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import platform
|
||||
import posixpath
|
||||
import threading
|
||||
from pathlib import Path
|
||||
import folder_paths # type: ignore
|
||||
@@ -25,6 +26,15 @@ standalone_mode = (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _normalize_root_identity(path: str) -> str:
|
||||
"""Normalize a root path for comparisons across slash styles."""
|
||||
|
||||
normalized = posixpath.normpath(path.strip().replace("\\", "/"))
|
||||
if len(normalized) >= 2 and normalized[1] == ":":
|
||||
return normalized.lower()
|
||||
return normalized
|
||||
|
||||
|
||||
def _resolve_valid_default_root(
|
||||
current: str, primary_paths: List[str], allowed_paths: List[str], name: str
|
||||
) -> str:
|
||||
@@ -37,14 +47,17 @@ def _resolve_valid_default_root(
|
||||
if not isinstance(path, str):
|
||||
continue
|
||||
stripped = path.strip()
|
||||
if not stripped or stripped in seen:
|
||||
if not stripped:
|
||||
continue
|
||||
seen.add(stripped)
|
||||
identity = _normalize_root_identity(stripped)
|
||||
if identity in seen:
|
||||
continue
|
||||
seen.add(identity)
|
||||
fallback_paths.append(stripped)
|
||||
|
||||
allowed = set(fallback_paths)
|
||||
allowed = {_normalize_root_identity(path) for path in fallback_paths}
|
||||
|
||||
if current and current in allowed:
|
||||
if current and _normalize_root_identity(current) in allowed:
|
||||
return current
|
||||
|
||||
if not valid_paths:
|
||||
|
||||
@@ -76,6 +76,9 @@ class TriggerWordToggleLM:
|
||||
# Filter out empty strings and return as set
|
||||
return set(word for word in words if word)
|
||||
|
||||
def _group_has_child_items(self, item):
|
||||
return isinstance(item, dict) and isinstance(item.get("items"), list)
|
||||
|
||||
def process_trigger_words(
|
||||
self,
|
||||
id,
|
||||
@@ -112,7 +115,11 @@ class TriggerWordToggleLM:
|
||||
|
||||
if isinstance(trigger_data, list):
|
||||
if group_mode:
|
||||
if allow_strength_adjustment:
|
||||
if any(self._group_has_child_items(item) for item in trigger_data):
|
||||
filtered_groups = self._process_group_items(
|
||||
trigger_data, allow_strength_adjustment
|
||||
)
|
||||
elif allow_strength_adjustment:
|
||||
parsed_items = [
|
||||
self._parse_trigger_item(
|
||||
item, allow_strength_adjustment
|
||||
@@ -174,6 +181,41 @@ class TriggerWordToggleLM:
|
||||
|
||||
return (filtered_triggers,)
|
||||
|
||||
def _process_group_items(self, trigger_data, allow_strength_adjustment):
|
||||
filtered_groups = []
|
||||
|
||||
for item in trigger_data:
|
||||
group = self._parse_trigger_item(item, allow_strength_adjustment)
|
||||
if not group["text"] or not group["active"]:
|
||||
continue
|
||||
|
||||
raw_items = item.get("items") if isinstance(item, dict) else None
|
||||
if isinstance(raw_items, list):
|
||||
active_items = []
|
||||
for raw_item in raw_items:
|
||||
child = self._parse_trigger_item(
|
||||
raw_item, allow_strength_adjustment=False
|
||||
)
|
||||
if child["text"] and child["active"]:
|
||||
active_items.append(child["text"])
|
||||
|
||||
if not active_items:
|
||||
continue
|
||||
|
||||
group_text = ", ".join(active_items)
|
||||
else:
|
||||
group_text = group["text"]
|
||||
|
||||
filtered_groups.append(
|
||||
self._format_word_output(
|
||||
group_text,
|
||||
group["strength"],
|
||||
allow_strength_adjustment,
|
||||
)
|
||||
)
|
||||
|
||||
return filtered_groups
|
||||
|
||||
def _parse_trigger_item(self, item, allow_strength_adjustment):
|
||||
text = (item.get("text") or "").strip()
|
||||
active = bool(item.get("active", False))
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
import shutil
|
||||
import tempfile
|
||||
import logging
|
||||
@@ -103,6 +104,15 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
|
||||
}
|
||||
|
||||
|
||||
def _normalize_root_identity(path: str) -> str:
|
||||
"""Normalize a root path for equality checks across slash styles."""
|
||||
|
||||
normalized = posixpath.normpath(path.strip().replace("\\", "/"))
|
||||
if len(normalized) >= 2 and normalized[1] == ":":
|
||||
return normalized.lower()
|
||||
return normalized
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
def __init__(self):
|
||||
self.settings_file = ensure_settings_file(logger)
|
||||
@@ -773,7 +783,7 @@ class SettingsManager:
|
||||
return False
|
||||
|
||||
allowed_roots = self._get_allowed_roots(key)
|
||||
if current and current in allowed_roots:
|
||||
if current and _normalize_root_identity(current) in allowed_roots:
|
||||
return False
|
||||
|
||||
self.settings[setting_key] = primary_candidates[0]
|
||||
@@ -824,16 +834,19 @@ class SettingsManager:
|
||||
if not isinstance(value, str):
|
||||
continue
|
||||
normalized = value.strip()
|
||||
if not normalized or normalized in seen:
|
||||
if not normalized:
|
||||
continue
|
||||
seen.add(normalized)
|
||||
identity = _normalize_root_identity(normalized)
|
||||
if identity in seen:
|
||||
continue
|
||||
seen.add(identity)
|
||||
candidates.append(normalized)
|
||||
return candidates
|
||||
|
||||
def _get_allowed_roots(self, key: str) -> set[str]:
|
||||
"""Return all valid roots for a model type, including extra roots."""
|
||||
|
||||
return set(self._get_valid_root_candidates(key))
|
||||
return {_normalize_root_identity(path) for path in self._get_valid_root_candidates(key)}
|
||||
|
||||
def _check_environment_variables(self) -> None:
|
||||
"""Check for environment variables and update settings if needed"""
|
||||
|
||||
@@ -402,6 +402,49 @@ def test_save_paths_keeps_default_roots_in_extra_paths(monkeypatch: pytest.Monke
|
||||
assert fake_settings.payload["activate"] is True
|
||||
|
||||
|
||||
def test_save_paths_keeps_default_roots_in_extra_paths_with_windows_slash_mismatch(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path
|
||||
):
|
||||
folder_paths = _setup_config_environment(monkeypatch, tmp_path)
|
||||
|
||||
class FakeSettingsService:
|
||||
active_library = "comfyui"
|
||||
|
||||
def get_libraries(self):
|
||||
return {
|
||||
"comfyui": {
|
||||
"folder_paths": {key: list(value) for key, value in folder_paths.items()},
|
||||
"extra_folder_paths": {
|
||||
"loras": ["U:\\Lora7\\Loras"],
|
||||
"checkpoints": ["U:\\Lora7\\Models"],
|
||||
"embeddings": [],
|
||||
},
|
||||
"default_lora_root": "U:/Lora7/Loras",
|
||||
"default_checkpoint_root": "U:/Lora7/Models",
|
||||
"default_embedding_root": folder_paths["embeddings"][0],
|
||||
}
|
||||
}
|
||||
|
||||
def rename_library(self, *_):
|
||||
raise AssertionError("rename_library should not be invoked")
|
||||
|
||||
def get_active_library_name(self):
|
||||
return self.active_library
|
||||
|
||||
def upsert_library(self, name: str, **payload):
|
||||
self.name = name
|
||||
self.payload = payload
|
||||
|
||||
fake_settings = FakeSettingsService()
|
||||
monkeypatch.setattr(settings_manager_module, "settings", fake_settings)
|
||||
|
||||
config_module.Config()
|
||||
|
||||
assert fake_settings.name == "comfyui"
|
||||
assert fake_settings.payload["default_lora_root"] == "U:/Lora7/Loras"
|
||||
assert fake_settings.payload["default_checkpoint_root"] == "U:/Lora7/Models"
|
||||
|
||||
|
||||
def test_save_paths_repairs_empty_default_roots_to_extra_paths_when_primary_missing(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path
|
||||
):
|
||||
|
||||
@@ -99,6 +99,99 @@ def test_duplicate_groups_respect_active_state():
|
||||
assert filtered == "A, B"
|
||||
|
||||
|
||||
def test_group_mode_can_exclude_individual_tags_within_active_group():
|
||||
node = TriggerWordToggleLM()
|
||||
trigger_data = [
|
||||
{
|
||||
"text": "outfit red, outfit blue, smiling",
|
||||
"active": True,
|
||||
"strength": None,
|
||||
"highlighted": False,
|
||||
"items": [
|
||||
{"text": "outfit red", "active": True, "highlighted": False},
|
||||
{"text": "outfit blue", "active": False, "highlighted": False},
|
||||
{"text": "smiling", "active": True, "highlighted": False},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
(filtered,) = node.process_trigger_words(
|
||||
id="node",
|
||||
group_mode=True,
|
||||
default_active=True,
|
||||
allow_strength_adjustment=False,
|
||||
orinalMessage="outfit red, outfit blue, smiling",
|
||||
toggle_trigger_words=trigger_data,
|
||||
)
|
||||
|
||||
assert filtered == "outfit red, smiling"
|
||||
|
||||
|
||||
def test_group_mode_keeps_group_strength_when_individual_tags_are_excluded():
|
||||
node = TriggerWordToggleLM()
|
||||
trigger_data = [
|
||||
{
|
||||
"text": "(outfit red, outfit blue, smiling:1.15)",
|
||||
"active": True,
|
||||
"strength": 1.15,
|
||||
"highlighted": False,
|
||||
"items": [
|
||||
{"text": "outfit red", "active": True, "highlighted": False},
|
||||
{"text": "outfit blue", "active": False, "highlighted": False},
|
||||
{"text": "smiling", "active": True, "highlighted": False},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
(filtered,) = node.process_trigger_words(
|
||||
id="node",
|
||||
group_mode=True,
|
||||
default_active=True,
|
||||
allow_strength_adjustment=True,
|
||||
orinalMessage="outfit red, outfit blue, smiling",
|
||||
toggle_trigger_words=trigger_data,
|
||||
)
|
||||
|
||||
assert filtered == "(outfit red, smiling:1.15)"
|
||||
|
||||
|
||||
def test_group_mode_omits_group_when_all_children_are_disabled():
|
||||
node = TriggerWordToggleLM()
|
||||
trigger_data = [
|
||||
{
|
||||
"text": "A, B",
|
||||
"active": True,
|
||||
"strength": None,
|
||||
"highlighted": False,
|
||||
"items": [
|
||||
{"text": "A", "active": False, "highlighted": False},
|
||||
{"text": "B", "active": False, "highlighted": False},
|
||||
],
|
||||
},
|
||||
{
|
||||
"text": "C, D",
|
||||
"active": True,
|
||||
"strength": None,
|
||||
"highlighted": False,
|
||||
"items": [
|
||||
{"text": "C", "active": True, "highlighted": False},
|
||||
{"text": "D", "active": True, "highlighted": False},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
(filtered,) = node.process_trigger_words(
|
||||
id="node",
|
||||
group_mode=True,
|
||||
default_active=True,
|
||||
allow_strength_adjustment=False,
|
||||
orinalMessage="A, B,, C, D",
|
||||
toggle_trigger_words=trigger_data,
|
||||
)
|
||||
|
||||
assert filtered == "C, D"
|
||||
|
||||
|
||||
def test_trigger_words_override_different_from_original():
|
||||
node = TriggerWordToggleLM()
|
||||
trigger_data = [
|
||||
|
||||
@@ -359,6 +359,25 @@ def test_auto_set_default_roots_keeps_valid_extra_values(manager):
|
||||
assert manager.get("default_embedding_root") == "/extra-embeddings"
|
||||
|
||||
|
||||
def test_auto_set_default_roots_keeps_valid_extra_values_with_windows_slash_mismatch(manager):
|
||||
manager.settings["default_lora_root"] = "U:/Lora7/Loras"
|
||||
manager.settings["default_checkpoint_root"] = "U:/Lora7/Models"
|
||||
|
||||
manager.settings["folder_paths"] = {
|
||||
"loras": ["R:/ComfyUI/models/loras"],
|
||||
"checkpoints": ["R:/ComfyUI/models/checkpoints"],
|
||||
}
|
||||
manager.settings["extra_folder_paths"] = {
|
||||
"loras": ["U:\\Lora7\\Loras"],
|
||||
"checkpoints": ["U:\\Lora7\\Models"],
|
||||
}
|
||||
|
||||
manager._auto_set_default_roots()
|
||||
|
||||
assert manager.get("default_lora_root") == "U:/Lora7/Loras"
|
||||
assert manager.get("default_checkpoint_root") == "U:/Lora7/Models"
|
||||
|
||||
|
||||
def test_auto_set_default_roots_falls_back_to_extra_when_primary_missing(manager):
|
||||
manager.settings["default_lora_root"] = ""
|
||||
manager.settings["folder_paths"] = {"loras": []}
|
||||
|
||||
@@ -580,6 +580,38 @@ body.lm-lora-reordering * {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.lm-trigger-count-badge {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.lm-trigger-count-badge--edited {
|
||||
color: rgba(226, 232, 240, 0.92);
|
||||
}
|
||||
|
||||
.lm-trigger-group-edit-button {
|
||||
transition: opacity 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
|
||||
.lm-trigger-group-edit-button:hover {
|
||||
color: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
||||
.lm-trigger-group-editor {
|
||||
background: rgba(26, 32, 44, 0.95);
|
||||
border: 1px solid rgba(66, 153, 225, 0.24);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.lm-trigger-group-editor__title {
|
||||
color: rgba(226, 232, 240, 0.96);
|
||||
}
|
||||
|
||||
.lm-trigger-group-editor__subtitle,
|
||||
.lm-trigger-group-editor__empty {
|
||||
color: rgba(226, 232, 240, 0.72);
|
||||
}
|
||||
|
||||
/* Autocomplete styling */
|
||||
.lm-autocomplete-name {
|
||||
flex: 1;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,50 +4,81 @@ import { CONVERTED_TYPE, getNodeFromGraph } from "./utils.js";
|
||||
import { addTagsWidget } from "./tags_widget.js";
|
||||
import { getWheelSensitivity } from "./settings.js";
|
||||
|
||||
// TriggerWordToggle extension for ComfyUI
|
||||
app.registerExtension({
|
||||
name: "LoraManager.TriggerWordToggle",
|
||||
function normalizeTagText(text) {
|
||||
return typeof text === "string" ? text.trim().toLowerCase() : "";
|
||||
}
|
||||
|
||||
setup() {
|
||||
// Add message handler to listen for messages from Python
|
||||
api.addEventListener("trigger_word_update", (event) => {
|
||||
const { id, graph_id: graphId, message } = event.detail;
|
||||
this.handleTriggerWordUpdate(id, graphId, message);
|
||||
});
|
||||
},
|
||||
function splitTopLevelCommas(text) {
|
||||
if (typeof text !== "string" || !text.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "TriggerWord Toggle (LoraManager)") {
|
||||
// Enable widget serialization
|
||||
node.serialize_widgets = true;
|
||||
const parts = [];
|
||||
let current = "";
|
||||
let depth = 0;
|
||||
|
||||
node.addInput("trigger_words", 'string', {
|
||||
"shape": 7 // 7 is the shape of the optional input
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
// Wait for node to be properly initialized
|
||||
requestAnimationFrame(async () => {
|
||||
// Get the wheel sensitivity setting
|
||||
const wheelSensitivity = getWheelSensitivity();
|
||||
const groupModeWidget = node.widgets[0];
|
||||
const defaultActiveWidget = node.widgets[1];
|
||||
const strengthAdjustmentWidget = node.widgets[2];
|
||||
const initialStrengthAdjustment = Boolean(strengthAdjustmentWidget?.value);
|
||||
const trimmed = current.trim();
|
||||
if (trimmed) {
|
||||
parts.push(trimmed);
|
||||
}
|
||||
|
||||
// Get the widget object directly from the returned object
|
||||
const result = addTagsWidget(node, "toggle_trigger_words", {
|
||||
defaultVal: []
|
||||
}, null, wheelSensitivity, {
|
||||
allowStrengthAdjustment: initialStrengthAdjustment
|
||||
});
|
||||
return parts;
|
||||
}
|
||||
|
||||
node.tagWidget = result.widget;
|
||||
node.tagWidget.allowStrengthAdjustment = initialStrengthAdjustment;
|
||||
function isGroupTag(tag) {
|
||||
return Array.isArray(tag?.items);
|
||||
}
|
||||
|
||||
const normalizeTagText = (text) =>
|
||||
(typeof text === 'string' ? text.trim().toLowerCase() : '');
|
||||
function parseSerializedText(text) {
|
||||
const normalizedText = typeof text === "string" ? text.trim() : "";
|
||||
const strengthMatch = normalizedText.match(/^\((.+):([\d.]+)\)$/);
|
||||
if (!strengthMatch) {
|
||||
return {
|
||||
text: normalizedText,
|
||||
strength: null,
|
||||
};
|
||||
}
|
||||
|
||||
const collectHighlightTokens = (wordsArray) => {
|
||||
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) => {
|
||||
@@ -58,7 +89,7 @@ app.registerExtension({
|
||||
};
|
||||
|
||||
wordsArray.forEach((rawWord) => {
|
||||
if (typeof rawWord !== 'string') {
|
||||
if (typeof rawWord !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,22 +98,148 @@ app.registerExtension({
|
||||
const groupParts = rawWord.split(/,{2,}/);
|
||||
groupParts.forEach((groupPart) => {
|
||||
addToken(groupPart);
|
||||
groupPart.split(',').forEach(addToken);
|
||||
splitTopLevelCommas(groupPart).forEach(addToken);
|
||||
});
|
||||
|
||||
rawWord.split(',').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;
|
||||
if (!node.tagWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const highlightSet = node._highlightedTriggerWords || new Set();
|
||||
const updatedTags = (node.tagWidget.value || []).map(tag => ({
|
||||
...tag,
|
||||
highlighted: highlightSet.size > 0 && highlightSet.has(normalizeTagText(tag.text))
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -104,14 +261,12 @@ app.registerExtension({
|
||||
|
||||
node.applyTriggerHighlightState = applyHighlightState;
|
||||
|
||||
// Add hidden widget to store original message
|
||||
const hiddenWidget = node.addWidget('text', 'orinalMessage', '');
|
||||
const hiddenWidget = node.addWidget("text", "orinalMessage", "");
|
||||
hiddenWidget.type = CONVERTED_TYPE;
|
||||
hiddenWidget.hidden = true;
|
||||
hiddenWidget.computeSize = () => [0, -4];
|
||||
node.originalMessageWidget = hiddenWidget;
|
||||
|
||||
// Restore saved value if exists
|
||||
const tagWidgetIndex = node.widgets.indexOf(result.widget);
|
||||
const originalMessageWidgetIndex = node.widgets.indexOf(hiddenWidget);
|
||||
if (node.widgets_values && node.widgets_values.length > 0) {
|
||||
@@ -132,6 +287,7 @@ app.registerExtension({
|
||||
requestAnimationFrame(() => node.applyTriggerHighlightState?.());
|
||||
|
||||
groupModeWidget.callback = (value) => {
|
||||
node.tagWidget?.closeGroupEditor?.();
|
||||
if (node.originalMessageWidget?.value) {
|
||||
this.updateTagsBasedOnMode(
|
||||
node,
|
||||
@@ -140,26 +296,41 @@ app.registerExtension({
|
||||
Boolean(strengthAdjustmentWidget?.value)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
defaultActiveWidget.callback = (value) => {
|
||||
if (!node.tagWidget || !node.tagWidget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add callback for default_active widget
|
||||
defaultActiveWidget.callback = (value) => {
|
||||
// Set all existing tags' active state to the new value
|
||||
if (node.tagWidget && node.tagWidget.value) {
|
||||
const updatedTags = node.tagWidget.value.map(tag => ({
|
||||
const updatedTags = node.tagWidget.value.map((tag) => {
|
||||
if (!Array.isArray(tag.items)) {
|
||||
return {
|
||||
...tag,
|
||||
active: value
|
||||
}));
|
||||
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,
|
||||
@@ -170,28 +341,33 @@ app.registerExtension({
|
||||
};
|
||||
}
|
||||
|
||||
// Override the serializeValue method to properly format trigger words with strength
|
||||
const originalSerializeValue = result.widget.serializeValue;
|
||||
result.widget.serializeValue = function() {
|
||||
const value = this.value || [];
|
||||
// Transform the values to include strength in the proper format
|
||||
const transformedValue = value.map(tag => {
|
||||
// If strength is defined (even if it's 1.0), format as {text: "(original_text:strength)", ...}
|
||||
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)})`
|
||||
text: `(${tag.text}:${tag.strength.toFixed(2)})`,
|
||||
};
|
||||
}
|
||||
|
||||
return tag;
|
||||
});
|
||||
return transformedValue;
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Handle trigger word updates from Python
|
||||
handleTriggerWordUpdate(id, graphId, message) {
|
||||
const node = getNodeFromGraph(graphId, id);
|
||||
if (!node || node.comfyClass !== "TriggerWord Toggle (LoraManager)") {
|
||||
@@ -199,13 +375,11 @@ app.registerExtension({
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the original message for mode switching
|
||||
if (node.originalMessageWidget) {
|
||||
node.originalMessageWidget.value = message;
|
||||
}
|
||||
|
||||
if (node.tagWidget) {
|
||||
// Parse tags based on current group mode
|
||||
const groupMode = node.widgets[0] ? node.widgets[0].value : false;
|
||||
const allowStrengthAdjustment = Boolean(node.widgets[2]?.value);
|
||||
node.tagWidget.allowStrengthAdjustment = allowStrengthAdjustment;
|
||||
@@ -213,85 +387,60 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
|
||||
// Update tags display based on group mode
|
||||
updateTagsBasedOnMode(node, message, groupMode, allowStrengthAdjustment = false) {
|
||||
if (!node.tagWidget) return;
|
||||
if (!node.tagWidget) {
|
||||
return;
|
||||
}
|
||||
node.tagWidget.closeGroupEditor?.();
|
||||
node.tagWidget.allowStrengthAdjustment = allowStrengthAdjustment;
|
||||
|
||||
const existingTags = node.tagWidget.value || [];
|
||||
const existingTagState = existingTags.reduce((acc, tag) => {
|
||||
const key = tag.text;
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push({
|
||||
active: tag.active,
|
||||
strength: allowStrengthAdjustment ? tag.strength : null,
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
const consumeExistingState = (text) => {
|
||||
const states = existingTagState[text];
|
||||
if (states && states.length > 0) {
|
||||
return states.shift();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Get default active state from the widget
|
||||
const existingTags = (node.tagWidget.value || []).map(cloneTag);
|
||||
const defaultActive = node.widgets[1] ? node.widgets[1].value : true;
|
||||
|
||||
let tagArray = [];
|
||||
|
||||
if (groupMode) {
|
||||
if (message.trim() === '') {
|
||||
tagArray = [];
|
||||
}
|
||||
// Group mode: split by ',,' and treat each group as a single tag
|
||||
else if (message.includes(',,')) {
|
||||
const groups = message.split(/,{2,}/); // Match 2 or more consecutive commas
|
||||
tagArray = groups
|
||||
.map(group => group.trim())
|
||||
.filter(group => group)
|
||||
.map(group => {
|
||||
// Check if this group already exists with strength info
|
||||
const existing = consumeExistingState(group);
|
||||
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 : defaultActive,
|
||||
highlighted: false,
|
||||
strength: null,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
text: group,
|
||||
// Use existing values if available, otherwise use defaults
|
||||
active: existing ? existing.active : defaultActive,
|
||||
strength: existing ? existing.strength : null
|
||||
highlighted: false,
|
||||
strength: existing ? existing.strength : null,
|
||||
items,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// If no ',,' delimiter, treat the entire message as one group
|
||||
const existing = consumeExistingState(message.trim());
|
||||
tagArray = [{
|
||||
text: message.trim(),
|
||||
// Use existing values if available, otherwise use defaults
|
||||
active: existing ? existing.active : defaultActive,
|
||||
strength: existing ? existing.strength : null
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
// Normal mode: split by commas and treat each word as a separate tag
|
||||
tagArray = message
|
||||
.split(',')
|
||||
.map(word => word.trim())
|
||||
.filter(word => word)
|
||||
.map(word => {
|
||||
// Check if this word already exists with strength info
|
||||
const existing = consumeExistingState(word);
|
||||
const existingTagState = buildLegacyTagState(existingTags, allowStrengthAdjustment);
|
||||
tagArray = splitTopLevelCommas(message).map((word) => {
|
||||
const existing = consumeQueuedState(existingTagState, word);
|
||||
return {
|
||||
text: word,
|
||||
// Use existing values if available, otherwise use defaults
|
||||
active: existing ? existing.active : defaultActive,
|
||||
strength: existing ? existing.strength : null
|
||||
highlighted: false,
|
||||
strength: existing ? existing.strength : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
node.tagWidget.value = tagArray;
|
||||
node.applyTriggerHighlightState?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user