diff --git a/py/nodes/trigger_word_toggle.py b/py/nodes/trigger_word_toggle.py index c0a7462f..99318341 100644 --- a/py/nodes/trigger_word_toggle.py +++ b/py/nodes/trigger_word_toggle.py @@ -23,6 +23,10 @@ class TriggerWordToggle: "default": True, "tooltip": "Sets the default initial state (active or inactive) when trigger words are added." }), + "allow_strength_adjustment": ("BOOLEAN", { + "default": False, + "tooltip": "Enable mouse wheel adjustment of each trigger word's strength." + }), }, "optional": FlexibleOptionalInputType(any_type), "hidden": { @@ -47,7 +51,14 @@ class TriggerWordToggle: else: return data - def process_trigger_words(self, id, group_mode, default_active, **kwargs): + def process_trigger_words( + self, + id, + group_mode, + default_active, + allow_strength_adjustment=False, + **kwargs, + ): # Handle both old and new formats for trigger_words trigger_words_data = self._get_toggle_data(kwargs, 'orinalMessage') trigger_words = trigger_words_data if isinstance(trigger_words_data, str) else "" @@ -73,50 +84,60 @@ class TriggerWordToggle: # 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) + original_word = strength_match.group(1).strip() strength = float(strength_match.group(2)) active_state[original_word] = active - strength_map[original_word] = strength + if allow_strength_adjustment: + strength_map[original_word] = strength else: - active_state[text] = active - + active_state[text.strip()] = active + if group_mode: - # Split by two or more consecutive commas to get groups - groups = re.split(r',{2,}', trigger_words) - # Remove leading/trailing whitespace from each group - groups = [group.strip() for group in groups] - - # 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) + if isinstance(trigger_data, list): + filtered_groups = [] + for item in trigger_data: + text = (item.get('text') or "").strip() + if not text: + continue + if item.get('active', False): + filtered_groups.append(text) + + if filtered_groups: + filtered_triggers = ', '.join(filtered_groups) + else: + filtered_triggers = "" else: - filtered_triggers = "" + # Split by two or more consecutive commas to get groups + groups = re.split(r',{2,}', trigger_words) + # Remove leading/trailing whitespace from each group + groups = [group.strip() for group in groups] + + # 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: + word_comparison = re.sub(r'\((.+):([\d.]+)\)', r'\1', word).strip() + + if word_comparison not in active_state or active_state[word_comparison]: + active_group_words.append( + self._format_word_output( + word_comparison, + strength_map, + allow_strength_adjustment, + ) + ) + + if active_group_words: + filtered_groups.append(', '.join(active_group_words)) + + if filtered_groups: + filtered_triggers = ', '.join(filtered_groups) + else: + filtered_triggers = "" else: # Normal mode: split by commas and treat each word as a separate tag original_words = [word.strip() for word in trigger_words.split(',')] @@ -129,17 +150,13 @@ class TriggerWordToggle: 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) + filtered_words.append( + self._format_word_output( + word_comparison, + strength_map, + allow_strength_adjustment, + ) + ) if filtered_words: filtered_triggers = ', '.join(filtered_words) @@ -149,4 +166,9 @@ class TriggerWordToggle: except Exception as e: logger.error(f"Error processing trigger words: {e}") - return (filtered_triggers,) \ No newline at end of file + return (filtered_triggers,) + + def _format_word_output(self, base_word, strength_map, allow_strength_adjustment): + if allow_strength_adjustment and base_word in strength_map: + return f"({base_word}:{strength_map[base_word]:.2f})" + return base_word diff --git a/tests/nodes/test_trigger_word_toggle.py b/tests/nodes/test_trigger_word_toggle.py new file mode 100644 index 00000000..64a92463 --- /dev/null +++ b/tests/nodes/test_trigger_word_toggle.py @@ -0,0 +1,26 @@ +from py.nodes.trigger_word_toggle import TriggerWordToggle + + +def test_group_mode_preserves_parenthesized_groups(): + node = TriggerWordToggle() + trigger_data = [ + {'text': 'flat color, dark theme', 'active': True, 'strength': None, 'highlighted': False}, + {'text': '(a, really, long, test, trigger, word:1.06)', 'active': True, 'strength': 1.06, 'highlighted': False}, + {'text': '(sinozick style:0.94)', 'active': True, 'strength': 0.94, 'highlighted': False}, + ] + + original_message = ( + "flat color, dark theme, (a, really, long, test, trigger, word:1.06), " + "(sinozick style:0.94)" + ) + + filtered, = node.process_trigger_words( + id="node", + group_mode=True, + default_active=True, + allow_strength_adjustment=False, + orinalMessage=original_message, + toggle_trigger_words=trigger_data, + ) + + assert filtered == original_message diff --git a/web/comfyui/tags_widget.js b/web/comfyui/tags_widget.js index f595ab60..2f2bb5a1 100644 --- a/web/comfyui/tags_widget.js +++ b/web/comfyui/tags_widget.js @@ -1,10 +1,12 @@ import { forwardMiddleMouseToCanvas } from "./utils.js"; -export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.02) { +export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.02, options = {}) { // Create container for tags const container = document.createElement("div"); container.className = "comfy-tags-container"; + const { allowStrengthAdjustment = true } = options; + forwardMiddleMouseToCanvas(container); // Set initial height @@ -41,6 +43,7 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0 } const normalizedTags = tagsData; + const showStrengthInfo = widget.allowStrengthAdjustment ?? allowStrengthAdjustment; if (normalizedTags.length === 0) { // Show message when no tags are present @@ -82,16 +85,44 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0 const tagEl = document.createElement("div"); tagEl.className = "comfy-tag"; - updateTagStyle(tagEl, active, highlighted, strength); + const textSpan = document.createElement("span"); + textSpan.className = "comfy-tag-text"; + textSpan.textContent = text; + Object.assign(textSpan.style, { + display: "inline-block", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + minWidth: "0", + flexGrow: "1", + }); + tagEl.appendChild(textSpan); - // 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; + const strengthBadge = showStrengthInfo ? document.createElement("span") : null; + if (strengthBadge) { + strengthBadge.className = "comfy-tag-strength"; + Object.assign(strengthBadge.style, { + fontSize: "11px", + fontWeight: "600", + padding: "1px 6px", + borderRadius: "999px", + letterSpacing: "0.2px", + backgroundColor: "rgba(255,255,255,0.08)", + color: "rgba(255,255,255,0.95)", + border: "1px solid rgba(255,255,255,0.25)", + lineHeight: "normal", + minWidth: "34px", + textAlign: "center", + pointerEvents: "none", + opacity: "0", + visibility: "hidden", + transition: "opacity 0.2s ease", + }); + tagEl.appendChild(strengthBadge); } - tagEl.title = text; // Set tooltip for full content + + updateTagStyle(tagEl, active, highlighted, strength); + updateStrengthDisplay(tagEl, strength, text, showStrengthInfo); // Add click handler to toggle state tagEl.addEventListener("click", (e) => { @@ -100,12 +131,14 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0 // Toggle active state for this specific tag using its index const updatedTags = [...widget.value]; updatedTags[index].active = !updatedTags[index].active; + textSpan.textContent = updatedTags[index].text; updateTagStyle( tagEl, updatedTags[index].active, updatedTags[index].highlighted, updatedTags[index].strength ); + updateStrengthDisplay(tagEl, updatedTags[index].strength, updatedTags[index].text); tagEl.dataset.active = updatedTags[index].active ? "true" : "false"; tagEl.dataset.highlighted = updatedTags[index].highlighted ? "true" : "false"; @@ -114,48 +147,42 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0 }); // Add mouse wheel handler to adjust strength - tagEl.addEventListener("wheel", (e) => { - e.preventDefault(); - e.stopPropagation(); + if (showStrengthInfo) { + 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; - } + // 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; - } + // 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); + // Ensure strength doesn't go below 0 + currentStrength = Math.max(0, currentStrength); - // Update the strength value - updatedTags[index].strength = currentStrength; + // Update the strength value + updatedTags[index].strength = currentStrength; + textSpan.textContent = updatedTags[index].text; - // 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)}`; + updateStrengthDisplay(tagEl, currentStrength, updatedTags[index].text, showStrengthInfo); - updateTagStyle( - tagEl, - updatedTags[index].active, - updatedTags[index].highlighted, - updatedTags[index].strength - ); - - widget.value = updatedTags; - }); + widget.value = updatedTags; + }); + } rowContainer.appendChild(tagEl); tagCount++; @@ -190,7 +217,7 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0 // Helper function to update tag style based on active state function updateTagStyle(tagEl, active, highlighted = false, strength = null) { const baseStyles = { - padding: "3px 10px", // Adjusted vertical padding to balance text + padding: "3px 10px", borderRadius: "6px", maxWidth: "200px", overflow: "hidden", @@ -200,7 +227,9 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0 cursor: "pointer", transition: "all 0.2s ease", border: "1px solid transparent", - display: "inline-block", // inline-block for better text truncation + display: "inline-flex", + alignItems: "center", + gap: "6px", boxShadow: "0 1px 2px rgba(0,0,0,0.1)", margin: "1px", userSelect: "none", @@ -214,7 +243,6 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0 maxWidth: "200px", lineHeight: "16px", // Added explicit line-height verticalAlign: "middle", // Added vertical alignment - position: "relative", // For better text positioning textAlign: "center", // Center text horizontally }; @@ -263,6 +291,42 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0 tagEl.dataset.highlighted = highlighted ? "true" : "false"; } + function formatStrengthValue(value) { + if (value === undefined || value === null) { + return null; + } + const num = Number(value); + if (!Number.isFinite(num)) { + return null; + } + return num.toFixed(2); + } + + function updateStrengthDisplay(tagEl, strength, baseText, showStrengthInfo) { + if (!showStrengthInfo) { + tagEl.title = baseText; + return; + } + const badge = tagEl.querySelector(".comfy-tag-strength"); + if (!badge) { + tagEl.title = baseText; + return; + } + const displayValue = strength === undefined || strength === null ? 1 : strength; + const formatted = formatStrengthValue(displayValue); + if (formatted !== null) { + badge.textContent = formatted; + badge.style.opacity = "1"; + badge.style.visibility = "visible"; + tagEl.title = `${baseText} (${formatted})`; + } else { + badge.textContent = ""; + badge.style.opacity = "0"; + badge.style.visibility = "hidden"; + tagEl.title = baseText; + } + } + // Store the value as array let widgetValue = initialTagsData; diff --git a/web/comfyui/trigger_word_toggle.js b/web/comfyui/trigger_word_toggle.js index 97d150e0..55d1baf1 100644 --- a/web/comfyui/trigger_word_toggle.js +++ b/web/comfyui/trigger_word_toggle.js @@ -75,13 +75,20 @@ app.registerExtension({ 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); // Get the widget object directly from the returned object const result = addTagsWidget(node, "toggle_trigger_words", { defaultVal: [] - }, null, wheelSensitivity); + }, null, wheelSensitivity, { + allowStrengthAdjustment: initialStrengthAdjustment + }); node.tagWidget = result.widget; + node.tagWidget.allowStrengthAdjustment = initialStrengthAdjustment; const normalizeTagText = (text) => (typeof text === 'string' ? text.trim().toLowerCase() : ''); @@ -148,31 +155,40 @@ app.registerExtension({ 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) { - // 0 is group mode, 1 is default_active, 2 is tag widget, 3 is original message - const savedValue = node.widgets_values[2]; - if (savedValue) { - result.widget.value = Array.isArray(savedValue) ? savedValue : []; + if (tagWidgetIndex >= 0) { + const savedValue = node.widgets_values[tagWidgetIndex]; + if (savedValue) { + result.widget.value = Array.isArray(savedValue) ? savedValue : []; + } } - const originalMessage = node.widgets_values[3]; - if (originalMessage) { - hiddenWidget.value = originalMessage; + if (originalMessageWidgetIndex >= 0) { + const originalMessage = node.widgets_values[originalMessageWidgetIndex]; + if (originalMessage) { + hiddenWidget.value = originalMessage; + } } } requestAnimationFrame(() => node.applyTriggerHighlightState?.()); - const groupModeWidget = node.widgets[0]; groupModeWidget.callback = (value) => { - if (node.widgets[3].value) { - this.updateTagsBasedOnMode(node, node.widgets[3].value, value); + if (node.originalMessageWidget?.value) { + this.updateTagsBasedOnMode( + node, + node.originalMessageWidget.value, + value, + Boolean(strengthAdjustmentWidget?.value) + ); } } // Add callback for default_active widget - const defaultActiveWidget = node.widgets[1]; defaultActiveWidget.callback = (value) => { // Set all existing tags' active state to the new value if (node.tagWidget && node.tagWidget.value) { @@ -185,6 +201,21 @@ app.registerExtension({ } } + if (strengthAdjustmentWidget) { + strengthAdjustmentWidget.callback = (value) => { + const allowStrengthAdjustment = Boolean(value); + if (node.tagWidget) { + node.tagWidget.allowStrengthAdjustment = allowStrengthAdjustment; + } + this.updateTagsBasedOnMode( + node, + node.originalMessageWidget?.value || "", + groupModeWidget?.value ?? false, + allowStrengthAdjustment + ); + }; + } + // Override the serializeValue method to properly format trigger words with strength const originalSerializeValue = result.widget.serializeValue; result.widget.serializeValue = function() { @@ -215,27 +246,32 @@ app.registerExtension({ } // Store the original message for mode switching - node.widgets[3].value = message; + 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; - this.updateTagsBasedOnMode(node, message, groupMode); + const allowStrengthAdjustment = Boolean(node.widgets[2]?.value); + node.tagWidget.allowStrengthAdjustment = allowStrengthAdjustment; + this.updateTagsBasedOnMode(node, message, groupMode, allowStrengthAdjustment); } }, // Update tags display based on group mode - updateTagsBasedOnMode(node, message, groupMode) { + updateTagsBasedOnMode(node, message, groupMode, allowStrengthAdjustment = false) { if (!node.tagWidget) return; + node.tagWidget.allowStrengthAdjustment = allowStrengthAdjustment; 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 + strength: allowStrengthAdjustment ? tag.strength : null }; });