interactive
This commit is contained in:
@@ -5,9 +5,9 @@ A ComfyUI custom node package for creating and applying multiple find/replace pa
|
||||
## Features
|
||||
|
||||
- **Find/Replace Pairs Node**: Create an arbitrary number of find/replace pairs
|
||||
- Supports up to 50 pairs per node
|
||||
- Each pair can have values entered directly or connected from other nodes
|
||||
- Dynamic UI shows only active pairs
|
||||
- Starts with one pair, click "➕ Add Pair" button to add more
|
||||
- Right-click menu also provides Add/Remove pair options
|
||||
- Each pair can be converted to an input connector (right-click widget → Convert to Input)
|
||||
- Outputs: pairs object, JSON, and CSV formats
|
||||
|
||||
- **Text Replacer Node**: Apply find/replace pairs to text
|
||||
@@ -32,15 +32,15 @@ A ComfyUI custom node package for creating and applying multiple find/replace pa
|
||||
### Basic Usage
|
||||
|
||||
1. Add a **Find/Replace Pairs** node
|
||||
2. Set the number of pairs you need with `pair_count`
|
||||
3. Enter your find patterns and replacements
|
||||
2. Enter your find pattern and replacement in the first pair
|
||||
3. Click **➕ Add Pair** to add more pairs as needed
|
||||
4. Connect the `pairs` output to a **Text Replacer** node
|
||||
5. Enter or connect your input text
|
||||
6. Get the modified text from the `result` output
|
||||
|
||||
### Connecting Dynamic Values
|
||||
|
||||
Each find/replace pair has optional input connectors (`find_N_input` and `replace_N_input`) that can receive values from other nodes. Connected inputs take priority over widget values.
|
||||
Right-click on any find or replace widget and select "Convert to Input" to create a connector that can receive values from other nodes.
|
||||
|
||||
### Chaining Replacers
|
||||
|
||||
@@ -65,11 +65,12 @@ The Text Replacer node passes through the pairs object, allowing you to chain mu
|
||||
|
||||
| Input | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `pair_count` | INT | Number of active pairs (1-50) |
|
||||
| `find_N` | STRING | Find pattern for pair N |
|
||||
| `replace_N` | STRING | Replacement for pair N |
|
||||
| `find_N_input` | STRING (connector) | Optional connected find pattern |
|
||||
| `replace_N_input` | STRING (connector) | Optional connected replacement |
|
||||
|
||||
**Buttons:**
|
||||
- **➕ Add Pair** - Add a new find/replace pair
|
||||
- **➖ Remove Last Pair** - Remove the last pair (disabled when only 1 pair exists)
|
||||
|
||||
### Text Replacer
|
||||
|
||||
|
||||
+145
-57
@@ -1,18 +1,16 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
|
||||
/**
|
||||
* ComfyUI Multi-Replace Extension
|
||||
* Handles dynamic visibility of find/replace pair inputs based on pair_count
|
||||
* Handles dynamic addition of find/replace pair inputs
|
||||
*/
|
||||
|
||||
const MAX_PAIRS = 50;
|
||||
|
||||
app.registerExtension({
|
||||
name: "comfyui-multi-replace",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "FindReplacePairs") {
|
||||
// Store original onNodeCreated
|
||||
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
@@ -20,84 +18,174 @@ app.registerExtension({
|
||||
origOnNodeCreated.apply(this, arguments);
|
||||
}
|
||||
|
||||
// Find the pair_count widget
|
||||
const pairCountWidget = this.widgets?.find(w => w.name === "pair_count");
|
||||
this.pairCount = 1;
|
||||
this.serialize_widgets = true;
|
||||
|
||||
if (pairCountWidget) {
|
||||
// Store original callback
|
||||
const origCallback = pairCountWidget.callback;
|
||||
// Add the "Add Pair" button
|
||||
this.addWidget("button", "➕ Add Pair", null, () => {
|
||||
this.addNewPair();
|
||||
});
|
||||
|
||||
// Override callback to update visibility
|
||||
pairCountWidget.callback = (value, ...args) => {
|
||||
if (origCallback) {
|
||||
origCallback.call(pairCountWidget, value, ...args);
|
||||
}
|
||||
this.updatePairVisibility(value);
|
||||
};
|
||||
// Add the "Remove Last Pair" button
|
||||
this.addWidget("button", "➖ Remove Last Pair", null, () => {
|
||||
this.removeLastPair();
|
||||
});
|
||||
|
||||
// Initial visibility update
|
||||
setTimeout(() => {
|
||||
this.updatePairVisibility(pairCountWidget.value);
|
||||
}, 100);
|
||||
}
|
||||
// Store reference to button widgets
|
||||
this.addPairButton = this.widgets[this.widgets.length - 2];
|
||||
this.removePairButton = this.widgets[this.widgets.length - 1];
|
||||
|
||||
this.updateRemoveButtonState();
|
||||
};
|
||||
|
||||
// Method to update visibility of pair widgets
|
||||
nodeType.prototype.updatePairVisibility = function(pairCount) {
|
||||
if (!this.widgets) return;
|
||||
// Add a new find/replace pair
|
||||
nodeType.prototype.addNewPair = function() {
|
||||
this.pairCount++;
|
||||
const idx = this.pairCount;
|
||||
|
||||
const count = parseInt(pairCount) || 1;
|
||||
// Insert new widgets before the buttons
|
||||
const buttonIndex = this.widgets.indexOf(this.addPairButton);
|
||||
|
||||
for (const widget of this.widgets) {
|
||||
const match = widget.name.match(/^(find|replace)_(\d+)$/);
|
||||
if (match) {
|
||||
const pairIndex = parseInt(match[2]);
|
||||
// Show widgets for active pairs + 1 empty pair for new entry
|
||||
const shouldShow = pairIndex <= count;
|
||||
// Create find widget
|
||||
const findWidget = ComfyWidgets.STRING(
|
||||
this,
|
||||
`find_${idx}`,
|
||||
["STRING", { default: "", multiline: false, placeholder: `Find pattern ${idx}` }],
|
||||
app
|
||||
).widget;
|
||||
|
||||
// ComfyUI uses different methods to hide widgets
|
||||
if (shouldShow) {
|
||||
widget.type = widget._originalType || widget.type;
|
||||
widget.computeSize = widget._originalComputeSize;
|
||||
} else {
|
||||
widget._originalType = widget._originalType || widget.type;
|
||||
widget._originalComputeSize = widget._originalComputeSize || widget.computeSize;
|
||||
widget.type = "hidden";
|
||||
widget.computeSize = () => [0, -4];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create replace widget
|
||||
const replaceWidget = ComfyWidgets.STRING(
|
||||
this,
|
||||
`replace_${idx}`,
|
||||
["STRING", { default: "", multiline: false, placeholder: `Replacement ${idx}` }],
|
||||
app
|
||||
).widget;
|
||||
|
||||
// Recalculate node size
|
||||
// Move the new widgets before the buttons
|
||||
// Remove from end and insert at button position
|
||||
this.widgets.splice(this.widgets.length - 2, 2); // Remove the 2 new widgets from end
|
||||
this.widgets.splice(buttonIndex, 0, findWidget, replaceWidget);
|
||||
|
||||
// Re-add buttons at end
|
||||
this.widgets.push(this.addPairButton, this.removePairButton);
|
||||
|
||||
this.updateRemoveButtonState();
|
||||
this.setSize(this.computeSize());
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
// Override onConfigure to restore visibility on load
|
||||
// Remove the last find/replace pair
|
||||
nodeType.prototype.removeLastPair = function() {
|
||||
if (this.pairCount <= 1) return;
|
||||
|
||||
const idx = this.pairCount;
|
||||
const findName = `find_${idx}`;
|
||||
const replaceName = `replace_${idx}`;
|
||||
|
||||
// Find and remove the widgets
|
||||
this.widgets = this.widgets.filter(w =>
|
||||
w.name !== findName && w.name !== replaceName
|
||||
);
|
||||
|
||||
// Remove inputs if they exist
|
||||
if (this.inputs) {
|
||||
this.inputs = this.inputs.filter(input =>
|
||||
input.name !== findName && input.name !== replaceName
|
||||
);
|
||||
}
|
||||
|
||||
this.pairCount--;
|
||||
this.updateRemoveButtonState();
|
||||
this.setSize(this.computeSize());
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
// Update remove button enabled state
|
||||
nodeType.prototype.updateRemoveButtonState = function() {
|
||||
if (this.removePairButton) {
|
||||
this.removePairButton.disabled = this.pairCount <= 1;
|
||||
}
|
||||
};
|
||||
|
||||
// Override getExtraMenuOptions to add right-click options
|
||||
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function(_, options) {
|
||||
if (origGetExtraMenuOptions) {
|
||||
origGetExtraMenuOptions.apply(this, arguments);
|
||||
}
|
||||
|
||||
options.unshift(
|
||||
{
|
||||
content: "Add Find/Replace Pair",
|
||||
callback: () => this.addNewPair()
|
||||
},
|
||||
{
|
||||
content: "Remove Last Pair",
|
||||
disabled: this.pairCount <= 1,
|
||||
callback: () => this.removeLastPair()
|
||||
},
|
||||
null // separator
|
||||
);
|
||||
};
|
||||
|
||||
// Handle serialization to save dynamic widgets
|
||||
const origOnSerialize = nodeType.prototype.onSerialize;
|
||||
nodeType.prototype.onSerialize = function(info) {
|
||||
if (origOnSerialize) {
|
||||
origOnSerialize.apply(this, arguments);
|
||||
}
|
||||
info.pairCount = this.pairCount;
|
||||
};
|
||||
|
||||
// Handle deserialization to restore dynamic widgets
|
||||
const origOnConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function(config) {
|
||||
nodeType.prototype.onConfigure = function(info) {
|
||||
// Restore additional pairs before calling original
|
||||
if (info.pairCount && info.pairCount > 1) {
|
||||
const targetCount = info.pairCount;
|
||||
|
||||
// Add pairs up to saved count
|
||||
while (this.pairCount < targetCount) {
|
||||
this.addNewPair();
|
||||
}
|
||||
}
|
||||
|
||||
if (origOnConfigure) {
|
||||
origOnConfigure.apply(this, arguments);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const pairCountWidget = this.widgets?.find(w => w.name === "pair_count");
|
||||
if (pairCountWidget) {
|
||||
this.updatePairVisibility(pairCountWidget.value);
|
||||
// Restore widget values from saved state
|
||||
if (info.widgets_values) {
|
||||
let valueIdx = 0;
|
||||
for (const widget of this.widgets) {
|
||||
if (widget.type !== "button" && valueIdx < info.widgets_values.length) {
|
||||
widget.value = info.widgets_values[valueIdx];
|
||||
valueIdx++;
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// Allow converting widgets to inputs for connections
|
||||
const origOnInputAdded = nodeType.prototype.onInputAdded;
|
||||
nodeType.prototype.onInputAdded = function(input) {
|
||||
if (origOnInputAdded) {
|
||||
origOnInputAdded.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "FindReplacePairs") {
|
||||
// Additional setup after node is fully created
|
||||
const pairCountWidget = node.widgets?.find(w => w.name === "pair_count");
|
||||
if (pairCountWidget) {
|
||||
setTimeout(() => {
|
||||
node.updatePairVisibility?.(pairCountWidget.value);
|
||||
}, 200);
|
||||
// Enable converting widgets to inputs (right-click -> Convert to Input)
|
||||
if (node.widgets) {
|
||||
for (const widget of node.widgets) {
|
||||
if (widget.name?.startsWith("find_") || widget.name?.startsWith("replace_")) {
|
||||
widget.convertToInput = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-62
@@ -4,32 +4,33 @@ Creates a collection of find/replace pairs with dynamic inputs.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
|
||||
|
||||
class FindReplacePairs:
|
||||
"""
|
||||
A node that creates an arbitrary number of find/replace pairs.
|
||||
Each pair can have values entered directly or connected from other nodes.
|
||||
Pairs are added dynamically via UI button.
|
||||
"""
|
||||
|
||||
MAX_PAIRS = 50 # Maximum number of pairs supported
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""
|
||||
Define dynamic inputs for find/replace pairs.
|
||||
We define a base set of inputs plus many optional pair inputs.
|
||||
Define base inputs. Additional pairs are added dynamically via JS.
|
||||
"""
|
||||
inputs = {
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"pair_count": ("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": cls.MAX_PAIRS,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
"find_1": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"placeholder": "Find pattern 1"
|
||||
}),
|
||||
"replace_1": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"placeholder": "Replacement 1"
|
||||
}),
|
||||
},
|
||||
"hidden": {
|
||||
@@ -37,28 +38,6 @@ class FindReplacePairs:
|
||||
}
|
||||
}
|
||||
|
||||
# Add optional inputs for each potential pair
|
||||
for i in range(1, cls.MAX_PAIRS + 1):
|
||||
inputs["optional"][f"find_{i}"] = ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"placeholder": f"Find pattern {i}"
|
||||
})
|
||||
inputs["optional"][f"replace_{i}"] = ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"placeholder": f"Replacement {i}"
|
||||
})
|
||||
# Also allow connected string inputs (these override the widget values)
|
||||
inputs["optional"][f"find_{i}_input"] = ("STRING", {
|
||||
"forceInput": True,
|
||||
})
|
||||
inputs["optional"][f"replace_{i}_input"] = ("STRING", {
|
||||
"forceInput": True,
|
||||
})
|
||||
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("FR_PAIRS", "STRING", "STRING",)
|
||||
RETURN_NAMES = ("pairs", "json_output", "csv_output",)
|
||||
OUTPUT_TOOLTIPS = (
|
||||
@@ -71,36 +50,32 @@ class FindReplacePairs:
|
||||
CATEGORY = "text/replace"
|
||||
DESCRIPTION = "Creates a collection of find/replace pairs that can be used with the TextReplacer node."
|
||||
|
||||
def create_pairs(self, pair_count=1, unique_id=None, **kwargs):
|
||||
def create_pairs(self, unique_id=None, **kwargs):
|
||||
"""
|
||||
Create the find/replace pairs collection.
|
||||
|
||||
Args:
|
||||
pair_count: Number of active pairs
|
||||
unique_id: Node's unique ID
|
||||
**kwargs: Dynamic find_N, replace_N, find_N_input, replace_N_input values
|
||||
**kwargs: Dynamic find_N, replace_N values
|
||||
|
||||
Returns:
|
||||
Tuple of (pairs_dict, json_string, csv_string)
|
||||
"""
|
||||
pairs = []
|
||||
|
||||
for i in range(1, pair_count + 1):
|
||||
# Check for connected input first, fall back to widget value
|
||||
find_key = f"find_{i}_input"
|
||||
find_widget_key = f"find_{i}"
|
||||
replace_key = f"replace_{i}_input"
|
||||
replace_widget_key = f"replace_{i}"
|
||||
# Collect all pairs from kwargs (find_1, replace_1, find_2, replace_2, etc.)
|
||||
pair_indices = set()
|
||||
for key in kwargs:
|
||||
if key.startswith("find_") or key.startswith("replace_"):
|
||||
try:
|
||||
idx = int(key.split("_")[1])
|
||||
pair_indices.add(idx)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
# Get find value (connected input takes priority)
|
||||
find_value = kwargs.get(find_key)
|
||||
if find_value is None or find_value == "":
|
||||
find_value = kwargs.get(find_widget_key, "")
|
||||
|
||||
# Get replace value (connected input takes priority)
|
||||
replace_value = kwargs.get(replace_key)
|
||||
if replace_value is None or replace_value == "":
|
||||
replace_value = kwargs.get(replace_widget_key, "")
|
||||
for i in sorted(pair_indices):
|
||||
find_value = kwargs.get(f"find_{i}", "")
|
||||
replace_value = kwargs.get(f"replace_{i}", "")
|
||||
|
||||
# Only add non-empty find patterns
|
||||
if find_value:
|
||||
@@ -136,13 +111,12 @@ class FindReplacePairs:
|
||||
return (pairs_obj, json_output, csv_output)
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, pair_count=1, **kwargs):
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
"""Force re-evaluation when inputs change."""
|
||||
# Create a hash of all input values
|
||||
values = [str(pair_count)]
|
||||
for i in range(1, pair_count + 1):
|
||||
values.append(str(kwargs.get(f"find_{i}", "")))
|
||||
values.append(str(kwargs.get(f"replace_{i}", "")))
|
||||
values.append(str(kwargs.get(f"find_{i}_input", "")))
|
||||
values.append(str(kwargs.get(f"replace_{i}_input", "")))
|
||||
return hash(tuple(values))
|
||||
return hash(str(kwargs))
|
||||
|
||||
|
||||
# API route to get node info (optional, for debugging)
|
||||
@PromptServer.instance.routes.get("/multi_replace/info")
|
||||
async def get_info(request):
|
||||
return web.json_response({"status": "ok", "name": "multi-replace"})
|
||||
|
||||
Reference in New Issue
Block a user