From dd27411ebf13e107de981f6a948a4c0e3616d8b4 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Fri, 7 Nov 2025 16:38:04 +0800 Subject: [PATCH] feat(trigger-word-toggle): add strength value support for trigger words - Extract and preserve strength values from trigger words in format "(word:strength)" - Maintain strength formatting when filtering active trigger words in both group and individual modes - Update active state tracking to handle strength-modified words correctly - Ensure backward compatibility with existing trigger word formats --- py/nodes/trigger_word_toggle.py | 66 +++++++++- web/comfyui/tags_widget.js | 73 ++++++++++- web/comfyui/trigger_word_toggle.js | 191 +++++++++++++++++++++-------- 3 files changed, 265 insertions(+), 65 deletions(-) diff --git a/py/nodes/trigger_word_toggle.py b/py/nodes/trigger_word_toggle.py index ea0c1b2d..c0a7462f 100644 --- a/py/nodes/trigger_word_toggle.py +++ b/py/nodes/trigger_word_toggle.py @@ -63,7 +63,22 @@ class TriggerWordToggle: trigger_data = json.loads(trigger_data) # Create dictionaries to track active state of words or groups - active_state = {item['text']: item.get('active', False) for item in trigger_data} + # Also track strength values for each trigger word + active_state = {} + strength_map = {} + + for item in trigger_data: + text = item['text'] + active = item.get('active', False) + # Extract strength if it's in the format "(word:strength)" + strength_match = re.match(r'\((.+):([\d.]+)\)', text) + if strength_match: + original_word = strength_match.group(1) + strength = float(strength_match.group(2)) + active_state[original_word] = active + strength_map[original_word] = strength + else: + active_state[text] = active if group_mode: # Split by two or more consecutive commas to get groups @@ -71,19 +86,60 @@ class TriggerWordToggle: # Remove leading/trailing whitespace from each group groups = [group.strip() for group in groups] - # Filter groups: keep those not in toggle_trigger_words or those that are active - filtered_groups = [group for group in groups if group not in active_state or active_state[group]] + # Process groups: keep those not in toggle_trigger_words or those that are active + filtered_groups = [] + for group in groups: + # Check if this group contains any words that are in the active_state + group_words = [word.strip() for word in group.split(',')] + active_group_words = [] + + for word in group_words: + # Remove any existing strength formatting for comparison + word_comparison = re.sub(r'\((.+):([\d.]+)\)', r'\1', word).strip() + + if word_comparison not in active_state or active_state[word_comparison]: + # If this word has a strength value, use that instead of the original + if word_comparison in strength_map: + active_group_words.append(f"({word_comparison}:{strength_map[word_comparison]:.2f})") + else: + # Preserve existing strength formatting if the word was previously modified + # Check if the original word had strength formatting + strength_match = re.match(r'\((.+):([\d.]+)\)', word) + if strength_match: + active_group_words.append(word) + else: + active_group_words.append(word) + + if active_group_words: + filtered_groups.append(', '.join(active_group_words)) if filtered_groups: filtered_triggers = ', '.join(filtered_groups) else: filtered_triggers = "" else: - # Original behavior for individual words mode + # Normal mode: split by commas and treat each word as a separate tag original_words = [word.strip() for word in trigger_words.split(',')] # Filter out empty strings original_words = [word for word in original_words if word] - filtered_words = [word for word in original_words if word not in active_state or active_state[word]] + + filtered_words = [] + for word in original_words: + # Remove any existing strength formatting for comparison + word_comparison = re.sub(r'\((.+):([\d.]+)\)', r'\1', word).strip() + + if word_comparison not in active_state or active_state[word_comparison]: + # If this word has a strength value, use that instead of the original + if word_comparison in strength_map: + filtered_words.append(f"({word_comparison}:{strength_map[word_comparison]:.2f})") + else: + # Preserve existing strength formatting if the word was previously modified + # Check if the original word had strength formatting + strength_match = re.match(r'\((.+):([\d.]+)\)', word) + if strength_match: + filtered_words.append(word) + else: + filtered_words.append(word) if filtered_words: filtered_triggers = ', '.join(filtered_words) diff --git a/web/comfyui/tags_widget.js b/web/comfyui/tags_widget.js index 648f7fc1..79968d25 100644 --- a/web/comfyui/tags_widget.js +++ b/web/comfyui/tags_widget.js @@ -1,6 +1,6 @@ import { forwardMiddleMouseToCanvas } from "./utils.js"; -export function addTagsWidget(node, name, opts, callback) { +export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.02) { // Create container for tags const container = document.createElement("div"); container.className = "comfy-tags-container"; @@ -78,13 +78,19 @@ export function addTagsWidget(node, name, opts, callback) { let tagCount = 0; normalizedTags.forEach((tagData, index) => { - const { text, active, highlighted } = tagData; + const { text, active, highlighted, strength } = tagData; const tagEl = document.createElement("div"); tagEl.className = "comfy-tag"; - updateTagStyle(tagEl, active, highlighted); + updateTagStyle(tagEl, active, highlighted, strength); - tagEl.textContent = text; + // Set the text content to include strength if present + // Always show strength if it has been modified to avoid layout shift + if (strength !== undefined && strength !== null) { + tagEl.textContent = `${text}:${strength.toFixed(2)}`; + } else { + tagEl.textContent = text; + } tagEl.title = text; // Set tooltip for full content // Add click handler to toggle state @@ -97,7 +103,8 @@ export function addTagsWidget(node, name, opts, callback) { updateTagStyle( tagEl, updatedTags[index].active, - updatedTags[index].highlighted + updatedTags[index].highlighted, + updatedTags[index].strength ); tagEl.dataset.active = updatedTags[index].active ? "true" : "false"; @@ -106,6 +113,50 @@ export function addTagsWidget(node, name, opts, callback) { widget.value = updatedTags; }); + // Add mouse wheel handler to adjust strength + tagEl.addEventListener("wheel", (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Only adjust strength if the mouse is over the tag + const updatedTags = [...widget.value]; + let currentStrength = updatedTags[index].strength; + + // If no strength is set, default to 1.0 + if (currentStrength === undefined || currentStrength === null) { + currentStrength = 1.0; + } + + // Adjust strength based on scroll direction + // DeltaY < 0 is scroll up, deltaY > 0 is scroll down + if (e.deltaY < 0) { + // Scroll up: increase strength by wheelSensitivity + currentStrength += wheelSensitivity; + } else { + // Scroll down: decrease strength by wheelSensitivity + currentStrength -= wheelSensitivity; + } + + // Ensure strength doesn't go below 0 + currentStrength = Math.max(0, currentStrength); + + // Update the strength value + updatedTags[index].strength = currentStrength; + + // Update the tag display to show the strength value + // Always show strength once it has been modified to avoid layout shift + tagEl.textContent = `${updatedTags[index].text}:${currentStrength.toFixed(2)}`; + + updateTagStyle( + tagEl, + updatedTags[index].active, + updatedTags[index].highlighted, + updatedTags[index].strength + ); + + widget.value = updatedTags; + }); + rowContainer.appendChild(tagEl); tagCount++; }); @@ -137,7 +188,7 @@ export function addTagsWidget(node, name, opts, callback) { }; // Helper function to update tag style based on active state - function updateTagStyle(tagEl, active, highlighted = false) { + function updateTagStyle(tagEl, active, highlighted = false, strength = null) { const baseStyles = { padding: "3px 10px", // Adjusted vertical padding to balance text borderRadius: "6px", @@ -178,6 +229,14 @@ export function addTagsWidget(node, name, opts, callback) { backgroundImage: "none", }; + // Additional styles for tags with modified strength + const strengthStyles = (strength !== null && strength !== undefined && strength !== 1.0) + ? { + border: "1px solid rgba(255, 215, 0, 0.7)", // Gold border for modified strength + backgroundImage: "linear-gradient(120deg, rgba(255,215,0,0.1), rgba(255,215,0,0.05))", + } + : {}; + if (active) { Object.assign(tagEl.style, { ...baseStyles, @@ -185,6 +244,7 @@ export function addTagsWidget(node, name, opts, callback) { color: "white", borderColor: "rgba(66, 153, 225, 0.9)", ...highlightStyles, + ...strengthStyles, }); } else { Object.assign(tagEl.style, { @@ -193,6 +253,7 @@ export function addTagsWidget(node, name, opts, callback) { color: "rgba(226, 232, 240, 0.8)", borderColor: "rgba(226, 232, 240, 0.2)", ...highlightStyles, + ...strengthStyles, }); } diff --git a/web/comfyui/trigger_word_toggle.js b/web/comfyui/trigger_word_toggle.js index 30ddf450..97d150e0 100644 --- a/web/comfyui/trigger_word_toggle.js +++ b/web/comfyui/trigger_word_toggle.js @@ -3,10 +3,57 @@ import { api } from "../../scripts/api.js"; import { CONVERTED_TYPE, getNodeFromGraph } from "./utils.js"; import { addTagsWidget } from "./tags_widget.js"; +// Setting ID for wheel sensitivity +const TRIGGER_WORD_WHEEL_SENSITIVITY_ID = "loramanager.trigger_word_wheel_sensitivity"; +const TRIGGER_WORD_WHEEL_SENSITIVITY_DEFAULT = 0.02; + +// Get the wheel sensitivity setting value +const getWheelSensitivity = (() => { + let settingsUnavailableLogged = false; + + return () => { + const settingManager = app?.extensionManager?.setting; + if (!settingManager || typeof settingManager.get !== "function") { + if (!settingsUnavailableLogged) { + console.warn("LoRA Manager: settings API unavailable, using default wheel sensitivity."); + settingsUnavailableLogged = true; + } + return TRIGGER_WORD_WHEEL_SENSITIVITY_DEFAULT; + } + + try { + const value = settingManager.get(TRIGGER_WORD_WHEEL_SENSITIVITY_ID); + return value ?? TRIGGER_WORD_WHEEL_SENSITIVITY_DEFAULT; + } catch (error) { + if (!settingsUnavailableLogged) { + console.warn("LoRA Manager: unable to read wheel sensitivity setting, using default.", error); + settingsUnavailableLogged = true; + } + return TRIGGER_WORD_WHEEL_SENSITIVITY_DEFAULT; + } + }; +})(); + // TriggerWordToggle extension for ComfyUI app.registerExtension({ name: "LoraManager.TriggerWordToggle", + settings: [ + { + id: TRIGGER_WORD_WHEEL_SENSITIVITY_ID, + name: "Trigger Word Wheel Sensitivity", + type: "slider", + attrs: { + min: 0.01, + max: 0.1, + step: 0.01, + }, + defaultValue: TRIGGER_WORD_WHEEL_SENSITIVITY_DEFAULT, + tooltip: "Mouse wheel sensitivity for adjusting trigger word strength (default: 0.02)", + category: ["LoRA Manager", "Trigger Word Toggle", "Wheel Sensitivity"], + }, + ], + setup() { // Add message handler to listen for messages from Python api.addEventListener("trigger_word_update", (event) => { @@ -26,10 +73,13 @@ app.registerExtension({ // Wait for node to be properly initialized requestAnimationFrame(async () => { + // Get the wheel sensitivity setting + const wheelSensitivity = getWheelSensitivity(); + // Get the widget object directly from the returned object const result = addTagsWidget(node, "toggle_trigger_words", { defaultVal: [] - }); + }, null, wheelSensitivity); node.tagWidget = result.widget; @@ -134,6 +184,24 @@ app.registerExtension({ node.applyTriggerHighlightState?.(); } } + + // 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)", ...} + if (tag.strength !== undefined && tag.strength !== null) { + return { + ...tag, + text: `(${tag.text}:${tag.strength.toFixed(2)})` + }; + } + return tag; + }); + return transformedValue; + }; }); } }, @@ -157,59 +225,74 @@ app.registerExtension({ }, // Update tags display based on group mode - updateTagsBasedOnMode(node, message, groupMode) { - if (!node.tagWidget) return; - - const existingTags = node.tagWidget.value || []; - const existingTagMap = {}; - - // Create a map of existing tags and their active states - existingTags.forEach(tag => { - existingTagMap[tag.text] = tag.active; + updateTagsBasedOnMode(node, message, groupMode) { + if (!node.tagWidget) return; + + const existingTags = node.tagWidget.value || []; + const existingTagMap = {}; + + // Create a map of existing tags and their active states and strengths + existingTags.forEach(tag => { + existingTagMap[tag.text] = { + active: tag.active, + strength: tag.strength + }; + }); + + // Get default active state from the widget + 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 = existingTagMap[group]; + return { + text: group, + // Use existing values if available, otherwise use defaults + active: existing ? existing.active : defaultActive, + strength: existing ? existing.strength : null + }; + }); + } else { + // If no ',,' delimiter, treat the entire message as one group + const existing = existingTagMap[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 = existingTagMap[word]; + return { + text: word, + // Use existing values if available, otherwise use defaults + active: existing ? existing.active : defaultActive, + strength: existing ? existing.strength : null + }; }); - - // Get default active state from the widget - 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 => ({ - text: group, - // Use defaultActive only for new tags - active: existingTagMap[group] !== undefined ? existingTagMap[group] : defaultActive - })); - } else { - // If no ',,' delimiter, treat the entire message as one group - tagArray = [{ - text: message.trim(), - // Use defaultActive only for new tags - active: existingTagMap[message.trim()] !== undefined ? existingTagMap[message.trim()] : defaultActive - }]; - } - } 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 => ({ - text: word, - // Use defaultActive only for new tags - active: existingTagMap[word] !== undefined ? existingTagMap[word] : defaultActive - })); - } - - node.tagWidget.value = tagArray; - node.applyTriggerHighlightState?.(); } + + node.tagWidget.value = tagArray; + node.applyTriggerHighlightState?.(); + } });