From 0ff0add436a2f7f4c1caf7514403e4d9a2785591 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:53:22 +0500 Subject: [PATCH 1/6] README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f5263b7..d1e5565 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ # `String Constructor` (Text-Formatting) nodes There's already a plenty of string-formatting nodes for ComfyUI. But this node pack takes a different approach: -- First, you prepare a dictionary of named sub-strings (chunks of text). -- Then, right before `CLIP Text Encode`, you construct a final prompt from these parts, using python's [string formatting syntax](https://docs.python.org/3/library/string.html#format-examples)... **Don't panic!** You don't need to be a programmer - just look below for a quick introduction. +- First, you prepare a dictionary of named sub-strings (chunks of text with assigned keyword/tag to reference them with). +- Then, right before `CLIP Text Encode`, you construct a final prompt fom this dictionary of available parts, connected **as a single input**. The compilation itself is done using python's [string formatting syntax](https://docs.python.org/3/library/string.html#format-examples)... **Don't panic!** You don't need to be a programmer - just look below for a quick introduction. - Unlike many other _(giant uber-mega)_ node packs, this one strictly adheres to the modular philosophy: it's minimal and self-sufficient. [🔄 Updates ChangeLog](CHANGELOG.md) From 84ae142f0b0552726bf9fa841014e51ba1e9ca3a Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:22:15 +0500 Subject: [PATCH 2/6] cleanup: reuse the same input-dict definition + fix some formatting and small mistakes --- funcs_dict_add_string.py | 7 ++++--- node_dict_add_string.py | 11 +++-------- node_dict_from_text.py | 7 +++---- node_dict_preview.py | 2 +- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/funcs_dict_add_string.py b/funcs_dict_add_string.py index da61249..902d003 100644 --- a/funcs_dict_add_string.py +++ b/funcs_dict_add_string.py @@ -10,12 +10,13 @@ from frozendict import deepfreeze as _deepfreeze from .enums import T as _T from .funcs_common import _raise_from_errors_dict, _validate_key, _verify_input_dict + def set_dict_string( name: str, value: str, strip_lines=True, in_dict: _t.Dict[str, _T] = None, # unique_id: str = None, -): +) -> _t.Tuple[_t.Dict[str, _t.Union[_T, str]]]: """Update/append a string to the dict.""" - out_dict: _t.Dict[str, _T] = _verify_input_dict(in_dict) + out_dict: _t.Dict[str, _t.Union[_T, str]] = _verify_input_dict(in_dict) errors_dict: _t.Dict[_t.Any, str] = dict() name = _validate_key(name, errors_dict) _raise_from_errors_dict(errors_dict) @@ -26,4 +27,4 @@ def set_dict_string( ) new_dict: _t.Dict[str, str] = {name: value} out_dict.update(new_dict) - return (_deepfreeze(out_dict),) + return (_deepfreeze(out_dict), ) diff --git a/node_dict_add_string.py b/node_dict_add_string.py index 189c7ff..58d9c44 100644 --- a/node_dict_add_string.py +++ b/node_dict_add_string.py @@ -23,16 +23,13 @@ _input_types = _deepfreeze({ "Name of the substring inserted into the dict. " "It must comprise only of latin letters, digits and underscores + it can't start with a digit." )}), - 'cleanup': (_IO.BOOLEAN, {'default': True, 'label_on': 'extra spaces', 'label_off': 'no', 'tooltip': ( + 'cleanup': (_IO.BOOLEAN, {'default': True, 'label_on': 'leading/trailing spaces', 'label_off': 'no', 'tooltip': ( "When enabled, each line in the sub-string is stripped from any spaces at its start and end." )}), - 'string': (_IO.STRING, {'multiline': True, 'tooltip': "The actual sub-string to set into the dict."}), + 'string': (_IO.STRING, {'multiline': True, 'tooltip': "The actual sub-string to add into the dict."}), }, 'optional': { - 'str_dict': _DataTypes.input_str_dict(tooltip=( - "The dictionary to take named sub-strings from. It could be left unconnected, if the pattern doesn't reference " - "any sub-strings - then, this node acts exactly the same as a regular string-primitive node." - )), + 'str_dict': _DataTypes.input_str_dict(tooltip="An optional Format-Dictionary to extend/update."), }, # 'hidden': { # 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node @@ -46,8 +43,6 @@ class StringConstructorDictAddString: CATEGORY = _meta.category DESCRIPTION = _format_docstring(_cleandoc(__doc__)) - OUTPUT_NODE = True # Just to show the status message even if not connected to anything - FUNCTION = 'main' RETURN_TYPES = (_DataTypes.STR_DICT, ) RETURN_NAMES = (_DataTypes.STR_DICT.lower(), ) diff --git a/node_dict_from_text.py b/node_dict_from_text.py index b35bab7..62db62f 100644 --- a/node_dict_from_text.py +++ b/node_dict_from_text.py @@ -14,12 +14,13 @@ from . import _meta from .docstring_formatter import format_docstring as _format_docstring from .enums import DataTypes as _DataTypes from .funcs_dict_from_text import parse_dict_from_text as _parse_dict_from_text +from .node_dict_add_string import _input_types as _input_types_str # A tiny optimization by reusing the same immutable dict: _input_types = _deepfreeze({ 'required': { - 'cleanup': (_IO.BOOLEAN, {'default': True, 'label_on': 'extra spaces', 'label_off': 'no', 'tooltip': ( + 'cleanup': (_IO.BOOLEAN, {'default': True, 'label_on': 'leading/trailing spaces', 'label_off': 'no', 'tooltip': ( "When enabled, each line in each sub-string is stripped from any spaces at its start and end." )}), 'strings': (_IO.STRING, {'multiline': True, 'tooltip': ( @@ -32,9 +33,7 @@ _input_types = _deepfreeze({ )}), }, 'optional': { - 'str_dict': _DataTypes.input_str_dict(tooltip=( - "An optional input dictionary. If it's connected, it will be expanded/updated with new strings." - )), + 'str_dict': _input_types_str['optional']['str_dict'], }, 'hidden': { 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node diff --git a/node_dict_preview.py b/node_dict_preview.py index 64a4b17..48b17dd 100644 --- a/node_dict_preview.py +++ b/node_dict_preview.py @@ -17,7 +17,7 @@ from .funcs_dict_preview import preview_str_dict as _preview_str_dict # A tiny optimization by reusing the same immutable dict: _input_types = _deepfreeze({ 'required': { - 'str_dict': _DataTypes.input_str_dict(tooltip="A dictionary of named substrings."), + 'str_dict': _DataTypes.input_str_dict(tooltip="A Format-Dictionary."), }, # 'optional': {}, 'hidden': { From 7a9b1dd023ee5d7053846d4f306a72f97993bf92 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:54:53 +0500 Subject: [PATCH 3/6] `StringConstructorDictAddAny` --- CHANGELOG.md | 14 ++++++++--- README.md | 2 +- __init__.py | 3 +++ funcs_dict_add_any.py | 27 ++++++++++++++++++++ node_dict_add_any.py | 58 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 funcs_dict_add_any.py create mode 100644 node_dict_add_any.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d0b238..e3feb02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# `TODO` +- Recursive formatting. + +# v1.0.0 + +The minimally-complete version. + +New node: +- `Add ANY to Format-Dict (Advanced)` + # v0.0.1 MVP nodes: @@ -5,7 +15,3 @@ MVP nodes: - `Format-Dict from Text` - `Format-Dict Preview` - `String Formatter` - -⚠️ Active `TODO:` -- Recursive formatting. -- Append non-string to Format-Dict. diff --git a/README.md b/README.md index d1e5565..34ad443 100644 --- a/README.md +++ b/README.md @@ -24,4 +24,4 @@ For those who are unfamiliar with python's string formatting, the rules are very > [!NOTE] > For advanced users: > Internally, a built-in `str.format()` is called with keyword arguments from `str_dict`, which is literally just a dict... though, an immutable one. -> So any formatting patterns are available (like `{float_value:.3f}`). However, currently there are no nodes to append non-string values (planned, TODO). +> So any formatting patterns are available (like `{float_value:.3f}`) + `Add ANY to Format-Dict` node is there for exactly that. diff --git a/__init__.py b/__init__.py index 83000be..4c613d2 100644 --- a/__init__.py +++ b/__init__.py @@ -4,18 +4,21 @@ import typing as _t +from .node_dict_add_any import StringConstructorDictAddAny from .node_dict_add_string import StringConstructorDictAddString from .node_dict_from_text import StringConstructorDictFromText from .node_dict_preview import StringConstructorDictPreview from .node_formatter import StringConstructorFormatter NODE_CLASS_MAPPINGS: _t.Dict[str, type] = { + 'StringConstructorDictAddAny': StringConstructorDictAddAny, 'StringConstructorDictAddString': StringConstructorDictAddString, 'StringConstructorDictFromText': StringConstructorDictFromText, 'StringConstructorDictPreview': StringConstructorDictPreview, 'StringConstructorFormatter': StringConstructorFormatter, } NODE_DISPLAY_NAME_MAPPINGS: _t.Dict[str, str] = { + 'StringConstructorDictAddAny': "Add ANY to Format-Dict (Advanced)", 'StringConstructorDictAddString': "Add String to Format-Dict", 'StringConstructorDictFromText': "Format-Dict from Text", 'StringConstructorDictPreview': "Preview Format-Dict", diff --git a/funcs_dict_add_any.py b/funcs_dict_add_any.py new file mode 100644 index 0000000..67b4d63 --- /dev/null +++ b/funcs_dict_add_any.py @@ -0,0 +1,27 @@ +# encoding: utf-8 +""" +The actual code (MVC pattern) for `StringConstructorDictAddAny` node. +""" + +import typing as _t + +from frozendict import deepfreeze as _deepfreeze + +from .funcs_common import _raise_from_errors_dict, _validate_key, _verify_input_dict + + +def set_dict_any_type_item( + name: str, value: _t.Any = None, in_dict: _t.Dict[str, _t.Any] = None, + # unique_id: str = None, +) -> _t.Tuple[_t.Dict[str, _t.Any]]: + """Update/append an item of any type to the dict.""" + out_dict: _t.Dict[str, _t.Any] = _verify_input_dict(in_dict) + if value is None: + return (_deepfreeze(in_dict), ) # No need to create another dict instance if it's already frozen + + errors_dict: _t.Dict[_t.Any, str] = dict() + name = _validate_key(name, errors_dict) + _raise_from_errors_dict(errors_dict) + new_dict: _t.Dict[str, _t.Any] = {name: value} + out_dict.update(new_dict) + return (_deepfreeze(out_dict), ) diff --git a/node_dict_add_any.py b/node_dict_add_any.py new file mode 100644 index 0000000..93f912c --- /dev/null +++ b/node_dict_add_any.py @@ -0,0 +1,58 @@ +# encoding: utf-8 +""" +""" + +import typing as _t + +from inspect import cleandoc as _cleandoc + +from frozendict import deepfreeze as _deepfreeze + +from comfy.comfy_types.node_typing import IO as _IO + +from . import _meta +from .docstring_formatter import format_docstring as _format_docstring +from .enums import DataTypes as _DataTypes +from .funcs_dict_add_any import set_dict_any_type_item as _set_dict_any_type_item +from .node_dict_add_string import _input_types as _input_types_str + + +# A tiny optimization by reusing the same immutable dict: +_input_types = _deepfreeze({ + 'required': { + 'name': (_IO.STRING, {'tooltip': ( + "Name of the non-string item inserted into the dict. " + "It must comprise only of latin letters, digits and underscores + it can't start with a digit." + )}), + }, + 'optional': { + 'str_dict': _input_types_str['optional']['str_dict'], + 'value': (_IO.ANY, {"forceInput": True, 'tooltip': "The actual non-string item to add into the dict."}), + }, + # 'hidden': { + # 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node + # }, +}) + + +class StringConstructorDictAddAny: + """Add/update a non-string item to the Format-Dict - to do some advanced formatting.""" + NODE_NAME = 'StringConstructorDictAddAny' + CATEGORY = _meta.category + DESCRIPTION = _format_docstring(_cleandoc(__doc__)) + + FUNCTION = 'main' + RETURN_TYPES = (_DataTypes.STR_DICT, ) + RETURN_NAMES = (_DataTypes.STR_DICT.lower(), ) + # OUTPUT_TOOLTIPS = tuple() + + @classmethod + def INPUT_TYPES(cls): + return _input_types + + def main( + self, name: str, value: _t.Any = None, + str_dict: _t.Dict[str, _t.Any] = None, + # unique_id: str = None, + ): + return _set_dict_any_type_item(name, value, in_dict=str_dict) From 3683a610e2a7107664df67f07b40bb02dd3cdf18 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 21 Jul 2025 20:11:10 +0500 Subject: [PATCH 4/6] rename data type to just `DICT` - for compatibility with other nodes --- CHANGELOG.md | 2 ++ enums.py | 6 +++--- node_dict_add_any.py | 10 +++++----- node_dict_add_string.py | 10 +++++----- node_dict_from_text.py | 10 +++++----- node_dict_preview.py | 10 +++++----- node_formatter.py | 6 +++--- 7 files changed, 28 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3feb02..8aedfda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The minimally-complete version. New node: - `Add ANY to Format-Dict (Advanced)` +The Format-Dict data type name changed from `STR_DICT` to just `DICT` - for potential compatibility with other nodes. + # v0.0.1 MVP nodes: diff --git a/enums.py b/enums.py index 8bbbc83..5c759b0 100644 --- a/enums.py +++ b/enums.py @@ -22,7 +22,7 @@ class __BaseEnum(_StrEnum): class DataTypes(__BaseEnum): """Additional data-types defined byt the node pack.""" - STR_DICT = 'STR_DICT' + DICT = 'DICT' @staticmethod def __custom_input_type_dict(_dict_args: _t.Tuple[_t.Dict[str, T], ...], kwargs: _t.Dict[str, T]) -> _t.Dict[str, T]: @@ -34,5 +34,5 @@ class DataTypes(__BaseEnum): return res_dict @classmethod - def input_str_dict(cls, *_dicts: _t.Dict[str, T], **kwargs: T) -> _t.Tuple[_t.Union['DataTypes', str], _t.Dict[str, T]]: - return cls.STR_DICT, cls.__custom_input_type_dict(_dicts, kwargs) + def input_dict(cls, *_dicts: _t.Dict[str, T], **kwargs: T) -> _t.Tuple[_t.Union['DataTypes', str], _t.Dict[str, T]]: + return cls.DICT, cls.__custom_input_type_dict(_dicts, kwargs) diff --git a/node_dict_add_any.py b/node_dict_add_any.py index 93f912c..3196607 100644 --- a/node_dict_add_any.py +++ b/node_dict_add_any.py @@ -26,7 +26,7 @@ _input_types = _deepfreeze({ )}), }, 'optional': { - 'str_dict': _input_types_str['optional']['str_dict'], + 'dict': _input_types_str['optional']['dict'], 'value': (_IO.ANY, {"forceInput": True, 'tooltip': "The actual non-string item to add into the dict."}), }, # 'hidden': { @@ -42,8 +42,8 @@ class StringConstructorDictAddAny: DESCRIPTION = _format_docstring(_cleandoc(__doc__)) FUNCTION = 'main' - RETURN_TYPES = (_DataTypes.STR_DICT, ) - RETURN_NAMES = (_DataTypes.STR_DICT.lower(), ) + RETURN_TYPES = (_DataTypes.DICT, ) + RETURN_NAMES = (_DataTypes.DICT.lower(), ) # OUTPUT_TOOLTIPS = tuple() @classmethod @@ -52,7 +52,7 @@ class StringConstructorDictAddAny: def main( self, name: str, value: _t.Any = None, - str_dict: _t.Dict[str, _t.Any] = None, + dict: _t.Dict[str, _t.Any] = None, # unique_id: str = None, ): - return _set_dict_any_type_item(name, value, in_dict=str_dict) + return _set_dict_any_type_item(name, value, in_dict=dict) diff --git a/node_dict_add_string.py b/node_dict_add_string.py index 58d9c44..6de5a1c 100644 --- a/node_dict_add_string.py +++ b/node_dict_add_string.py @@ -29,7 +29,7 @@ _input_types = _deepfreeze({ 'string': (_IO.STRING, {'multiline': True, 'tooltip': "The actual sub-string to add into the dict."}), }, 'optional': { - 'str_dict': _DataTypes.input_str_dict(tooltip="An optional Format-Dictionary to extend/update."), + 'dict': _DataTypes.input_dict(tooltip="An optional Format-Dictionary to extend/update."), }, # 'hidden': { # 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node @@ -44,8 +44,8 @@ class StringConstructorDictAddString: DESCRIPTION = _format_docstring(_cleandoc(__doc__)) FUNCTION = 'main' - RETURN_TYPES = (_DataTypes.STR_DICT, ) - RETURN_NAMES = (_DataTypes.STR_DICT.lower(), ) + RETURN_TYPES = (_DataTypes.DICT, ) + RETURN_NAMES = (_DataTypes.DICT.lower(), ) # OUTPUT_TOOLTIPS = tuple() @classmethod @@ -54,7 +54,7 @@ class StringConstructorDictAddString: def main( self, name: str, cleanup: bool, string: str, - str_dict: _t.Dict[str, _t.Any] = None, + dict: _t.Dict[str, _t.Any] = None, # unique_id: str = None, ): - return _set_dict_string(name, string, strip_lines=cleanup, in_dict=str_dict) + return _set_dict_string(name, string, strip_lines=cleanup, in_dict=dict) diff --git a/node_dict_from_text.py b/node_dict_from_text.py index 62db62f..95ecb24 100644 --- a/node_dict_from_text.py +++ b/node_dict_from_text.py @@ -33,7 +33,7 @@ _input_types = _deepfreeze({ )}), }, 'optional': { - 'str_dict': _input_types_str['optional']['str_dict'], + 'dict': _input_types_str['optional']['dict'], }, 'hidden': { 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node @@ -52,8 +52,8 @@ class StringConstructorDictFromText: OUTPUT_NODE = True # Just to show the status message even if not connected to anything FUNCTION = 'main' - RETURN_TYPES = (_DataTypes.STR_DICT, ) - RETURN_NAMES = (_DataTypes.STR_DICT.lower(), ) + RETURN_TYPES = (_DataTypes.DICT, ) + RETURN_NAMES = (_DataTypes.DICT.lower(), ) # OUTPUT_TOOLTIPS = tuple() @classmethod @@ -62,7 +62,7 @@ class StringConstructorDictFromText: def main( self, cleanup: bool, strings: str, show_status: bool, - str_dict: _t.Dict[str, _t.Any] = None, + dict: _t.Dict[str, _t.Any] = None, unique_id: str = None, ): - return _parse_dict_from_text(strings, strip_lines=cleanup, show=show_status, in_dict=str_dict, unique_id=unique_id) + return _parse_dict_from_text(strings, strip_lines=cleanup, show=show_status, in_dict=dict, unique_id=unique_id) diff --git a/node_dict_preview.py b/node_dict_preview.py index 48b17dd..f5c4d21 100644 --- a/node_dict_preview.py +++ b/node_dict_preview.py @@ -17,7 +17,7 @@ from .funcs_dict_preview import preview_str_dict as _preview_str_dict # A tiny optimization by reusing the same immutable dict: _input_types = _deepfreeze({ 'required': { - 'str_dict': _DataTypes.input_str_dict(tooltip="A Format-Dictionary."), + 'dict': _DataTypes.input_dict(tooltip="A Format-Dictionary."), }, # 'optional': {}, 'hidden': { @@ -37,8 +37,8 @@ class StringConstructorDictPreview: OUTPUT_NODE = True FUNCTION = 'main' - RETURN_TYPES = (_DataTypes.STR_DICT, ) - RETURN_NAMES = (_DataTypes.STR_DICT.lower(), ) + RETURN_TYPES = (_DataTypes.DICT, ) + RETURN_NAMES = (_DataTypes.DICT.lower(), ) # OUTPUT_TOOLTIPS = tuple() @classmethod @@ -46,7 +46,7 @@ class StringConstructorDictPreview: return _input_types def main( - self, str_dict: _t.Dict[str, _t.Any], + self, dict: _t.Dict[str, _t.Any], unique_id: str = None, ): - return _preview_str_dict(str_dict, unique_id=unique_id) + return _preview_str_dict(dict, unique_id=unique_id) diff --git a/node_formatter.py b/node_formatter.py index c80fed2..79fc978 100644 --- a/node_formatter.py +++ b/node_formatter.py @@ -30,7 +30,7 @@ _input_types = _deepfreeze({ )}), }, 'optional': { - 'str_dict': _DataTypes.input_str_dict(tooltip=( + 'dict': _DataTypes.input_dict(tooltip=( "The dictionary to take named sub-strings from. It could be left unconnected, if the pattern doesn't reference " "any sub-strings - then, this node acts exactly the same as a regular string-primitive node." )), @@ -62,7 +62,7 @@ class StringConstructorFormatter: def main( self, template: str, show_status: bool, - str_dict: _t.Dict[str, _t.Any] = None, + dict: _t.Dict[str, _t.Any] = None, unique_id: str = None, ): - return _formatter(template, str_dict, show=show_status, unique_id=unique_id) + return _formatter(template, dict, show=show_status, unique_id=unique_id) From 39eb8ddc31bcd0edac9392f59d34dd0f35857041 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:53:19 +0500 Subject: [PATCH 5/6] TODO --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aedfda..42bace0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # `TODO` - Recursive formatting. +- Multi-item nodes. # v1.0.0 From 316b91d16a32413d1e771c2a82bfeec428ab54d1 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:53:57 +0500 Subject: [PATCH 6/6] `v1.0.0` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 57b13e6..41861fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "string-constructor" description = "Build your dictionary of named strings (text chunks), then easily compile different prompt variations from them - with simple string formatting.\nUnlike string-formatting nodes from other popular packs, this one is designed for specifying your sub-strings once, then dragging the entire \"library\" of them across the entire workflow (like a bus), updating it in-place as necessary. This is especially handy for regional prompting aka area composition." -version = "0.0.1" +version = "1.0.0" authors = [ {name = "Lex Darlog"} ]