feat: add V3 (comfy_api) dual-API wrappers; clear error on invalid regex; tooltips; bump 2.0.11
v3_nodes.py builds a V3 io.ComfyNode per V1 class from its INPUT_TYPES/ RETURN_TYPES and delegates execute() to the V1 method (no forked logic), including the StringPreview OUTPUT_NODE ui.PreviewText path. comfy_entrypoint exported import-guarded. StringMultiReplace use_regex now raises a clear ValueError naming the bad pattern instead of a raw re.error. Added tooltips to strip/multi_replace boolean flags. Verified ComfyUI 0.25.0.
This commit is contained in:
@@ -114,6 +114,10 @@ git clone https://github.com/bradsec/ComfyUI_StringEssentials.git
|
||||
|
||||
---
|
||||
|
||||
## Compatibility
|
||||
|
||||
Dual API: exports the legacy V1 `NODE_CLASS_MAPPINGS` and a V3 `comfy_entrypoint` (`comfy_api`). Verified on ComfyUI 0.25.0. On builds whose loader reads `NODE_CLASS_MAPPINGS` first, the V1 path is used and `comfy_entrypoint` is skipped, so nodes are not registered twice.
|
||||
|
||||
## Support
|
||||
|
||||
If you find this useful, please consider [starring the repo](https://github.com/bradsec/ComfyUI_StringEssentials). Stars help other people discover these nodes.
|
||||
|
||||
@@ -28,3 +28,12 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
WEB_DIRECTORY = "./js"
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
# V3 schema wrappers, exposed only when comfy_api is importable. On the 0.25.0
|
||||
# loader the V1 NODE_CLASS_MAPPINGS above win and comfy_entrypoint is skipped,
|
||||
# so the nodes are not registered twice.
|
||||
try:
|
||||
from .v3_nodes import comfy_entrypoint # noqa: F401
|
||||
__all__.append("comfy_entrypoint")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui_stringessentials"
|
||||
description = "Simple string manipulation nodes for ComfyUI (strip/remove text strings, search and replace text strings, preview modified string outputs). Useful for modifying text prompts or results from LLM outputs. Nodes will be located under standard Add Node > utils menu. Node names: String Textbox, String Strip, String Multi Replace, String Conditional Append, String Contains Any and String Preview."
|
||||
version = "2.0.10"
|
||||
version = "2.0.11"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -14,14 +14,17 @@ class StringMultiReplaceNode:
|
||||
"replacement_delimiter": ("STRING", {"default": "::",
|
||||
"tooltip": "Character(s) that separate search and replace strings. Default is two '::' (colons)"}),
|
||||
"match_case": ("BOOLEAN", {"default": False,
|
||||
"label_on": "enabled", "label_off": "disabled"}),
|
||||
"label_on": "enabled", "label_off": "disabled",
|
||||
"tooltip": "When enabled, matching is case-sensitive."}),
|
||||
"match_whole_string": ("BOOLEAN", {"default": False,
|
||||
"label_on": "enabled", "label_off": "disabled"}),
|
||||
"label_on": "enabled", "label_off": "disabled",
|
||||
"tooltip": "When enabled, only whole-word matches are replaced, not substrings inside other words."}),
|
||||
"preserve_punctuation": ("BOOLEAN", {"default": True,
|
||||
"label_on": "enabled", "label_off": "disabled",
|
||||
"tooltip": "When enabled, punctuation marks adjacent to matched text are preserved"}),
|
||||
"remove_extra_spaces": ("BOOLEAN", {"default": True,
|
||||
"label_on": "enabled", "label_off": "disabled"}),
|
||||
"label_on": "enabled", "label_off": "disabled",
|
||||
"tooltip": "When enabled, collapses repeated spaces and trims the result after replacement."}),
|
||||
"sort_by_length": ("BOOLEAN", {"default": True,
|
||||
"label_on": "enabled", "label_off": "disabled",
|
||||
"tooltip": "When enabled, longer search strings are replaced first to prevent substring clobbering. Disable for input-order replacement."}),
|
||||
@@ -83,7 +86,15 @@ class StringMultiReplaceNode:
|
||||
# Original behavior: remove punctuation after match
|
||||
pattern = escaped + r'(?:[,;:.])?'
|
||||
|
||||
regex = re.compile(pattern, flags=flags)
|
||||
try:
|
||||
regex = re.compile(pattern, flags=flags)
|
||||
except re.error as e:
|
||||
# use_regex lets the user supply raw regex; surface the bad
|
||||
# pattern and the reason instead of a bare traceback.
|
||||
raise ValueError(
|
||||
f"StringMultiReplace: invalid regex for search '{search_str}': {e}. "
|
||||
f"Disable use_regex to treat search strings as literal text."
|
||||
)
|
||||
result = regex.sub(replace_str, result)
|
||||
|
||||
if remove_extra_spaces:
|
||||
|
||||
@@ -12,14 +12,17 @@ class StringStripNode:
|
||||
"strings_to_remove": ("STRING", {"multiline": True, "default": "", "placeholder": "Enter strings to remove (one per line)",
|
||||
"tooltip": "Enter strings to remove (one per line). Can include multiple words or strings on one line like: 'This image is'. This will only match this entire string."}),
|
||||
"match_case": ("BOOLEAN", {"default": False,
|
||||
"label_on": "enabled", "label_off": "disabled"}),
|
||||
"label_on": "enabled", "label_off": "disabled",
|
||||
"tooltip": "When enabled, matching is case-sensitive."}),
|
||||
"match_whole_string": ("BOOLEAN", {"default": True,
|
||||
"label_on": "enabled", "label_off": "disabled"}),
|
||||
"label_on": "enabled", "label_off": "disabled",
|
||||
"tooltip": "When enabled, only whole-word matches are removed, not substrings inside other words."}),
|
||||
"preserve_punctuation": ("BOOLEAN", {"default": True,
|
||||
"label_on": "enabled", "label_off": "disabled",
|
||||
"tooltip": "When enabled, punctuation marks adjacent to matched text are preserved"}),
|
||||
"remove_extra_spaces": ("BOOLEAN", {"default": True,
|
||||
"label_on": "enabled", "label_off": "disabled"})
|
||||
"label_on": "enabled", "label_off": "disabled",
|
||||
"tooltip": "When enabled, collapses repeated spaces and trims the result after removal."})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
"""V3 (comfy_api schema) wrappers for the StringEssentials nodes.
|
||||
|
||||
Import-guarded: if comfy_api is unavailable (older ComfyUI), this module exposes
|
||||
nothing and the V1 NODE_CLASS_MAPPINGS in __init__.py remain the only path.
|
||||
|
||||
The wrappers are thin adapters. Each V3 node is built from the matching V1 class
|
||||
by reading its INPUT_TYPES / RETURN_TYPES / RETURN_NAMES, and its execute()
|
||||
delegates to the V1 class method, so the string logic is never forked.
|
||||
"""
|
||||
|
||||
try:
|
||||
from comfy_api.v0_0_2 import io, ui, ComfyExtension
|
||||
_V3_AVAILABLE = True
|
||||
except ImportError:
|
||||
_V3_AVAILABLE = False
|
||||
|
||||
|
||||
if _V3_AVAILABLE:
|
||||
|
||||
_OUT = {"STRING": io.String, "BOOLEAN": io.Boolean, "INT": io.Int}
|
||||
|
||||
def _to_input(name, spec, optional):
|
||||
t = spec[0]
|
||||
opts = spec[1] if len(spec) > 1 else {}
|
||||
if isinstance(t, list):
|
||||
return io.Combo.Input(name, options=t, default=opts.get("default"),
|
||||
tooltip=opts.get("tooltip"), optional=optional)
|
||||
if t == "STRING":
|
||||
return io.String.Input(name, default=opts.get("default", ""),
|
||||
multiline=opts.get("multiline", False),
|
||||
placeholder=opts.get("placeholder"),
|
||||
tooltip=opts.get("tooltip"),
|
||||
force_input=opts.get("forceInput"), optional=optional)
|
||||
if t == "INT":
|
||||
return io.Int.Input(name, default=opts.get("default", 0), min=opts.get("min"),
|
||||
max=opts.get("max"), step=opts.get("step"),
|
||||
tooltip=opts.get("tooltip"), optional=optional)
|
||||
if t == "BOOLEAN":
|
||||
return io.Boolean.Input(name, default=opts.get("default", False),
|
||||
label_on=opts.get("label_on"), label_off=opts.get("label_off"),
|
||||
tooltip=opts.get("tooltip"), optional=optional)
|
||||
raise ValueError(f"StringEssentials V3: unmapped input type {t!r} for {name}")
|
||||
|
||||
def _build_inputs(v1_cls):
|
||||
spec = v1_cls.INPUT_TYPES()
|
||||
inputs = []
|
||||
for section, optional in (("required", False), ("optional", True)):
|
||||
for name, s in spec.get(section, {}).items():
|
||||
inputs.append(_to_input(name, s, optional))
|
||||
return inputs
|
||||
|
||||
def _build_outputs(v1_cls):
|
||||
names = getattr(v1_cls, "RETURN_NAMES", None) or v1_cls.RETURN_TYPES
|
||||
return [_OUT[t].Output(id=n, display_name=n)
|
||||
for t, n in zip(v1_cls.RETURN_TYPES, names)]
|
||||
|
||||
def make_v3(node_id, display_name, v1_cls):
|
||||
func = v1_cls.FUNCTION
|
||||
is_output = getattr(v1_cls, "OUTPUT_NODE", False)
|
||||
desc = getattr(v1_cls, "DESCRIPTION", "")
|
||||
has_is_changed = hasattr(v1_cls, "IS_CHANGED")
|
||||
|
||||
class _V3(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id=node_id,
|
||||
display_name=display_name,
|
||||
category=v1_cls.CATEGORY,
|
||||
description=desc,
|
||||
inputs=_build_inputs(v1_cls),
|
||||
outputs=_build_outputs(v1_cls),
|
||||
is_output_node=is_output,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, **kwargs) -> io.NodeOutput:
|
||||
result = getattr(v1_cls(), func)(**kwargs)
|
||||
# OUTPUT_NODE preview returns {"ui": {"text": (...)}, "result": (...)}.
|
||||
if isinstance(result, dict):
|
||||
text = result.get("ui", {}).get("text", [""])
|
||||
res = result.get("result", ())
|
||||
return io.NodeOutput(*res, ui=ui.PreviewText(text[0] if text else ""))
|
||||
return io.NodeOutput(*result)
|
||||
|
||||
if has_is_changed:
|
||||
@classmethod
|
||||
def _fingerprint(cls, **kwargs):
|
||||
return v1_cls.IS_CHANGED(**kwargs)
|
||||
_V3.fingerprint_inputs = _fingerprint
|
||||
|
||||
_V3.__name__ = node_id + "V3"
|
||||
return _V3
|
||||
|
||||
class StringEssentialsExtension(ComfyExtension):
|
||||
async def get_node_list(self):
|
||||
# Imported at call time (after the package finished importing) to
|
||||
# avoid a circular import with __init__.py.
|
||||
from . import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
return [make_v3(nid, NODE_DISPLAY_NAME_MAPPINGS.get(nid, nid), cls)
|
||||
for nid, cls in NODE_CLASS_MAPPINGS.items()]
|
||||
|
||||
async def comfy_entrypoint() -> "StringEssentialsExtension":
|
||||
return StringEssentialsExtension()
|
||||
Reference in New Issue
Block a user