Merge branch 'dev': v0.0.1

This commit is contained in:
Lex Darlog (DRL)
2025-07-21 09:33:41 +05:00
20 changed files with 973 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
name: 📦 Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
# Just to be nice - to prevent unwanted auto-runs on forks made solely for a PR:
if: ${{ github.repository_owner == 'Lex-DRL' }}
steps:
- name: ♻️ Check out code
uses: actions/checkout@v4
- name: 📦 Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
# Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
# personal_access_token: {% raw %}${{ secrets.REGISTRY_ACCESS_TOKEN }}{% endraw %}
+11
View File
@@ -0,0 +1,11 @@
# v0.0.1
MVP nodes:
- `Add String to Format-Dict`
- `Format-Dict from Text`
- `Format-Dict Preview`
- `String Formatter`
⚠️ Active `TODO:`
- Recursive formatting.
- Append non-string to Format-Dict.
+157
View File
@@ -0,0 +1,157 @@
### GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates the
terms and conditions of version 3 of the GNU General Public License,
supplemented by the additional permissions listed below.
#### 0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the
GNU General Public License.
"The Library" refers to a covered work governed by this License, other
than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
#### 1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
#### 2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
- a) under this License, provided that you make a good faith effort
to ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
- b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
#### 3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from a
header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
- a) Give prominent notice with each copy of the object code that
the Library is used in it and that the Library and its use are
covered by this License.
- b) Accompany the object code with a copy of the GNU GPL and this
license document.
#### 4. Combined Works.
You may convey a Combined Work under terms of your choice that, taken
together, effectively do not restrict modification of the portions of
the Library contained in the Combined Work and reverse engineering for
debugging such modifications, if you also do each of the following:
- a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
- b) Accompany the Combined Work with a copy of the GNU GPL and this
license document.
- c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
- d) Do one of the following:
- 0) Convey the Minimal Corresponding Source under the terms of
this License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
- 1) Use a suitable shared library mechanism for linking with
the Library. A suitable mechanism is one that (a) uses at run
time a copy of the Library already present on the user's
computer system, and (b) will operate properly with a modified
version of the Library that is interface-compatible with the
Linked Version.
- e) Provide Installation Information, but only if you would
otherwise be required to provide such information under section 6
of the GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the Application
with a modified version of the Linked Version. (If you use option
4d0, the Installation Information must accompany the Minimal
Corresponding Source and Corresponding Application Code. If you
use option 4d1, you must provide the Installation Information in
the manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.)
#### 5. Combined Libraries.
You may place library facilities that are a work based on the Library
side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
- a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities, conveyed under the terms of this License.
- b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
#### 6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
as you received it specifies that a certain numbered version of the
GNU Lesser General Public License "or any later version" applies to
it, you have the option of following the terms and conditions either
of that published version or of any later version published by the
Free Software Foundation. If the Library as you received it does not
specify a version number of the GNU Lesser General Public License, you
may choose any version of the GNU Lesser General Public License ever
published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
+27
View File
@@ -0,0 +1,27 @@
> "Do one thing and do it well." _— Peter H. Salus / Doug McIlroy, [core Unix principle](https://en.wikipedia.org/wiki/Unix_philosophy)_
> "Simple is better than complex." _— Zen of Python_
# `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.
- 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)
![image](img/screenshot1.png)
## Syntax
For those who are unfamiliar with python's string formatting, the rules are very simple:
- You need to name (set tags/**keys** for) your sub-strings the same way python variables are named: only ASCII letters, digits and underscore are allowed + the name can't start with a digit. So:
- ✅ `valid_name`, `other_valid_name`, `YetAnother_ValidName`, `name3`.
- ❌ `wrong name with spaces`, `wrong-name.with:punctuation`, `3name`.
- Then, you simply put your `{substring_name}` inside curly braces (no spaces between) in the formatting template - and voila! This pattern will be replaced with the actual sub-string.
> [!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).
+25
View File
@@ -0,0 +1,25 @@
# encoding: utf-8
"""
"""
import typing as _t
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] = {
'StringConstructorDictAddString': StringConstructorDictAddString,
'StringConstructorDictFromText': StringConstructorDictFromText,
'StringConstructorDictPreview': StringConstructorDictPreview,
'StringConstructorFormatter': StringConstructorFormatter,
}
NODE_DISPLAY_NAME_MAPPINGS: _t.Dict[str, str] = {
'StringConstructorDictAddString': "Add String to Format-Dict",
'StringConstructorDictFromText': "Format-Dict from Text",
'StringConstructorDictPreview': "Preview Format-Dict",
'StringConstructorFormatter': "String Formatter",
}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
+6
View File
@@ -0,0 +1,6 @@
# encoding: utf-8
"""
Metadata-related module.
"""
category: str = "String Constructor"
+146
View File
@@ -0,0 +1,146 @@
# encoding: utf-8
"""
"""
import typing as _t
from inspect import getdoc as _getdoc
import re as _re
_re_indent_match = _re.compile("(\t*)( +)(\t*)(.*?)$").match
_re_tab_indent_match = _re.compile("(\t+)(.*?)$").match
_re_list_line_match = _re.compile(
"(\s*)("
"[-*•]+"
"|"
"[a-zA-Z]\s*[.)]"
"|"
"[0-9+]\s*[.)]"
")\s+"
).match
def _recover_tab_indents(line: str, tab_size: int):
"""Turn indenting spaces back to tabs using regexp. Half-tab indents are rounded."""
assert bool(line) and isinstance(line, str)
n_tabs = 0.0
match = _re_indent_match(line)
while match:
pre_tabs, spaces, post_tabs, line = match.groups()
n_tabs_from_spaces = float(len(spaces)) / tab_size + 0.00001
n_post_tabs = len(post_tabs)
if n_post_tabs > 0:
# There are tabs after spaces. Don't preserve the fractional spaces-indent, truncate it:
n_tabs_from_spaces = int(n_tabs_from_spaces)
n_tabs += len(pre_tabs) + n_tabs_from_spaces + n_post_tabs
match = _re_indent_match(line)
if n_tabs < 0.5:
return line
tabs_prefix = '\t' * int(n_tabs + 0.50001)
return f"{tabs_prefix}{line}"
def _join_paragraph_and_format_tabs(paragraph: _t.List[str], tab_size: int):
"""
Given "continuous" paragraph (i.e., with no empty newlines between chunks), recover tabs for each chunk
and join them together into a single actual line.
Works as a generator to account for blocks with different indents - to make each its own line.
"""
pending_indent = 0
pending_chunks: _t.List[str] = list()
def join_pending_chunks() -> str:
return "{}{}".format('\t' * pending_indent, ' '.join(pending_chunks))
for chunk in paragraph:
chunk = _recover_tab_indents(chunk, tab_size)
cur_indent = 0
match = _re_tab_indent_match(chunk)
if match:
tab_indent, chunk = match.groups() # We've detected indent. Now, get rid of it.
cur_indent = len(tab_indent)
match_list_line = _re_list_line_match(chunk)
# In case of a bulleted/numbered list, we'll need to start a new block, too.
if cur_indent == pending_indent and not match_list_line:
pending_chunks.append(chunk)
continue
# Indent mismatch or a list line:
# we're either ended one block or entered another. Either way, the previous block ends.
if pending_chunks:
yield join_pending_chunks()
pending_chunks = list()
assert not pending_chunks
pending_chunks.append(chunk)
pending_indent = cur_indent
if pending_chunks:
yield join_pending_chunks()
def _formatted_paragraphs_gen(doc: str, tab_size: int):
"""
Generator, which splits docstring into lines and transforms them into an actual printable output:
- From each bulk of empty lines, the first one is skipped...
- ... thus, non-empty lines are joined into continuous paragraphs.
- Recover tabs in the beginning oh lines (``inspect.cleandoc()`` converts them into spaces).
"""
if not doc:
return
doc = str(doc)
if not doc.strip():
return
tab_size = max(int(tab_size), 1)
cur_paragraph: _t.List[str] = list()
for line in doc.splitlines():
line: str = line.rstrip()
if line:
cur_paragraph.append(line)
continue
assert not line
if cur_paragraph:
for block in _join_paragraph_and_format_tabs(cur_paragraph, tab_size):
yield block
cur_paragraph = list()
# Just skip the current empty line entirely - do nothing with it.
continue
# We're in a chain of empty lines, and we've already skipped the first one. Preserve the remaining ones:
yield ''
# Return the last paragraph post-loop:
if cur_paragraph:
for block in _join_paragraph_and_format_tabs(cur_paragraph, tab_size):
yield block
def format_docstring(doc: str, tab_size: int = 8) -> str:
"""
Turn a pre-cleaned-up docstring (with tabs as spaces and newlines mid-sentence)
into an actually printable output:
- mid-paragraph new lines are replaced with spaces...
- ... while still keeping indented blocks separate.
Remember to pass a pre-cleaned-up docstring - i.e.: ``format_docstring(inspect.cleandoc(__doc__))``
"""
return '\n'.join(_formatted_paragraphs_gen(doc, tab_size))
def format_object_docstring(_obj, tab_size: int = 8) -> str:
"""Find the object's docstring and format it with ``format_docstring()``"""
doc = _getdoc(_obj)
if not doc:
return ''
# noinspection PyArgumentList
return format_docstring(doc, tab_size=tab_size)
+38
View File
@@ -0,0 +1,38 @@
# encoding: utf-8
"""
Internal enums.
"""
import typing as _t
try:
from enum import StrEnum as _StrEnum
except ImportError:
from comfy.comfy_types.node_typing import StrEnum as _StrEnum
T = _t.TypeVar('T')
class __BaseEnum(_StrEnum):
@classmethod
def all_values(cls) -> _t.Tuple[str, ...]:
return tuple(x.value for x in cls)
class DataTypes(__BaseEnum):
"""Additional data-types defined byt the node pack."""
STR_DICT = 'STR_DICT'
@staticmethod
def __custom_input_type_dict(_dict_args: _t.Tuple[_t.Dict[str, T], ...], kwargs: _t.Dict[str, T]) -> _t.Dict[str, T]:
res_dict: _t.Dict[str, _t.Any] = {"forceInput": True}
for d in _dict_args:
if d is not None:
res_dict.update(d)
res_dict.update(kwargs)
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)
+81
View File
@@ -0,0 +1,81 @@
# encoding: utf-8
"""
Code shared between various `funcs_*` modules.
"""
import typing as _t
import re as _re
from server import PromptServer as _PromptServer
from .enums import T as _T
def _show_text_on_node(text: str = None, unique_id: str = None):
if not text:
# TODO: Planned for the future - currently, there's no point removing the text since it's box is shown anyway
# An odd workaround since `send_progress_text()` doesn't want to update text when '' passed
text = '<span></span>'
# print(f"{unique_id} text: {text!r}")
# Snatched from: https://github.com/comfyanonymous/ComfyUI/blob/27870ec3c30e56be9707d89a120eb7f0e2836be1/comfy_extras/nodes_images.py#L581-L582
_PromptServer.instance.send_progress_text(text, unique_id)
_re_valid_key_match = _re.compile("[a-zA-Z_][a-zA-Z_0-9]*$").match
_re_starting_digits_match = _re.compile("[0-9]+").match
def _validate_key(key: str, errors_dict: _t.Dict[_t.Any, str]) -> _t.Union[str, None]:
"""Verify that the given string can be a key. Otherwise, append a message to `errors_dict`."""
if not isinstance(key, str):
errors_dict[key] = f"Not a valid key (string name): {key!r}"
return None
key = str(key).strip()
if not key:
errors_dict[key] = f"Key (string name) can't be empty. Got: {key!r}"
return None
if not _re_valid_key_match(key):
if _re_starting_digits_match(key):
errors_dict[key] = f"Key (string name) can't start with a digit. Got: {key!r}"
else:
errors_dict[key] = f"Key (string name) must contain only numbers, latin letters and underscores. Got: {key!r}"
return None
return key
def _raise_from_errors_dict(
errors_dict: _t.Dict[_t.Any, str],
single_error_format: str = '{}',
multi_errors_format: str = "Invalid keys (string names):\n{}",
):
if not errors_dict:
return
if len(errors_dict) == 1:
msg = single_error_format.format(next(iter(errors_dict.values())))
else:
msg = multi_errors_format.format('\n'.join(repr(x) for x in errors_dict.keys()))
raise KeyError(msg)
def _verify_input_dict(input_dict: _t.Dict[str, _T] = None) -> _t.Dict[str, _T]:
"""
Verify input dict to have only valid keys. Raises errors if invalid ones found.
Always returns a new dict instance (shallow copy if non-empty dict passed).
"""
if input_dict is None:
return dict()
if not isinstance(input_dict, dict):
raise TypeError(f"Input-dict isn't a dict. Got: {input_dict!r}")
errors_dict: _t.Dict[_t.Any, str] = dict()
for key in input_dict.keys():
_validate_key(key, errors_dict)
_raise_from_errors_dict(
errors_dict,
"Invalid input-dict:\n{}", "Invalid keys (string names) in input-dict:\n{}"
)
return dict(input_dict)
+29
View File
@@ -0,0 +1,29 @@
# encoding: utf-8
"""
The actual code (MVC pattern) for `StringConstructorDictString` node.
"""
import typing as _t
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,
):
"""Update/append a string to the dict."""
out_dict: _t.Dict[str, _T] = _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)
value = str(value)
if strip_lines:
value = '\n'.join(
x.strip() for x in value.splitlines()
)
new_dict: _t.Dict[str, str] = {name: value}
out_dict.update(new_dict)
return (_deepfreeze(out_dict),)
+81
View File
@@ -0,0 +1,81 @@
# encoding: utf-8
"""
The actual code (MVC pattern) for `StringConstructorDictFromText` node.
"""
import typing as _t
from frozendict import frozendict as _frozendict, deepfreeze as _deepfreeze
from .enums import T as _T
from .funcs_common import _show_text_on_node, _raise_from_errors_dict, _validate_key, _verify_input_dict
def _return_line_raw(line_raw: str, line_stripped:str) -> str:
return line_raw
def _return_line_stripped(line_raw: str, line_stripped:str) -> str:
return line_stripped
def _parsed_kv_pairs_gen(multiline_str: str, strip_lines=True):
"""
Given a multiline string, extract keywords (first non-empty line in each chunk)
and their substrings (all the following non-empty lines).
"""
if not multiline_str:
return
multiline_str = str(multiline_str)
if not multiline_str.strip():
return
appended_line_f = _return_line_stripped if strip_lines else _return_line_raw
cur_chunk: _t.List[str] = list()
def dump_chunk():
cur_chunk_iter = iter(cur_chunk) # It's more backwards-compatible than `a, *b = x`
chunk_key = next(cur_chunk_iter)
cur_chunk_lines = list(cur_chunk_iter)
# If there are no actual lines, join() would return an empty string:
return chunk_key.strip(), '\n'.join(cur_chunk_lines)
for line in multiline_str.splitlines():
line_stripped: str = line.strip()
if line_stripped:
cur_chunk.append(appended_line_f(line, line_stripped))
continue
assert not line_stripped
if cur_chunk:
yield dump_chunk()
cur_chunk = list()
# We simply skip all the empty lines entirely
if cur_chunk:
yield dump_chunk()
def parse_dict_from_text(
multiline_str: str, strip_lines=True,
show: bool = True, in_dict: _t.Dict[str, _T] = None, unique_id: str = None
) -> _t.Tuple[_t.Dict[str, _T]]:
out_dict: _t.Dict[str, _T] = _verify_input_dict(in_dict)
errors_dict: _t.Dict[_t.Any, str] = dict()
new_dict: _t.Dict[str, str] = {
_k: _v for _k, _v in (
(_validate_key(k, errors_dict), v)
for k, v in _parsed_kv_pairs_gen(multiline_str, strip_lines=strip_lines)
) if _k is not None
}
_raise_from_errors_dict(errors_dict)
out_dict.update(new_dict)
if show and unique_id:
status_text = ','.join(new_dict.keys()) if new_dict else ''
_show_text_on_node(status_text, unique_id)
return (_deepfreeze(out_dict), )
+47
View File
@@ -0,0 +1,47 @@
# encoding: utf-8
"""
The actual code (MVC pattern) for `StringConstructorDictPreview` node.
"""
import typing as _t
from pprint import pformat as _pformat
from .funcs_common import _show_text_on_node, _verify_input_dict
def _to_regular_dict_recursive_copy(input_dict: dict = None) -> dict:
"""
To simplify preview, turns frozen dicts back to regular ones.
Copies the dict in process and does so recursively, but not truly deep: if value is another dict,
it will be processed, too. But dicts within other objects (like lists) won't be found.
"""
if not input_dict:
return dict()
return {
k: (_to_regular_dict_recursive_copy(v) if isinstance(v, dict) else v)
for k, v in input_dict.items()
}
def _preview_single_entry(key: str, value) -> str:
if isinstance(value, str):
return f"{key}:\n{value}"
value_repr = _pformat(value)
return f"{key}:\n{value_repr}"
def _preview_message(input_dict: dict = None):
input_dict = _to_regular_dict_recursive_copy(_verify_input_dict(input_dict))
parts: _t.List[str] = [
_preview_single_entry(k, v) for k, v in input_dict.items()
]
return '\n\n'.join(parts)
def preview_str_dict(input_dict: dict, unique_id: str = None):
if not unique_id:
return (input_dict, )
status_text = _preview_message(input_dict)
_show_text_on_node(status_text, unique_id)
return (input_dict, )
+24
View File
@@ -0,0 +1,24 @@
# encoding: utf-8
"""
The actual code (MVC pattern) for `StringConstructorFormatter` node.
"""
import typing as _t
from .enums import T as _T
from .funcs_common import _show_text_on_node, _verify_input_dict
def formatter(template: str, format_dict: _t.Dict[str, _T] = None, show: bool = True, unique_id: str = None) -> _t.Tuple[str]:
format_dict = _verify_input_dict(format_dict)
if not isinstance(template, str):
raise TypeError(f"Not a string: {template!r}")
out_text = template.format_map(format_dict)
print(template)
print(format_dict)
print(out_text)
if show and unique_id:
_show_text_on_node(out_text, unique_id)
return (out_text, )
Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

+65
View File
@@ -0,0 +1,65 @@
# 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_string import set_dict_string as _set_dict_string
# A tiny optimization by reusing the same immutable dict:
_input_types = _deepfreeze({
'required': {
'name': (_IO.STRING, {'tooltip': (
"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': (
"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."}),
},
'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."
)),
},
# 'hidden': {
# 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node
# },
})
class StringConstructorDictAddString:
"""Add/update a string to the Format-Dict."""
NODE_NAME = '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(), )
# OUTPUT_TOOLTIPS = tuple()
@classmethod
def INPUT_TYPES(cls):
return _input_types
def main(
self, name: str, cleanup: bool, string: str,
str_dict: _t.Dict[str, _t.Any] = None,
# unique_id: str = None,
):
return _set_dict_string(name, string, strip_lines=cleanup, in_dict=str_dict)
+69
View File
@@ -0,0 +1,69 @@
# encoding: utf-8
"""
"""
import typing as _t
from inspect import cleandoc as _cleandoc
from frozendict import frozendict as _frozendict, 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_from_text import parse_dict_from_text as _parse_dict_from_text
# 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': (
"When enabled, each line in each sub-string is stripped from any spaces at its start and end."
)}),
'strings': (_IO.STRING, {'multiline': True, 'tooltip': (
"Sub-string names followed by their text. Different sub-string chunks are separated by empty lines. Example:\n\n"
"char1_short\n1boy, blond, short hair\n\n"
"char1_long\n1boy, smiling, blue eyes, blond, short hair,\nsitting on a motorcycle, wearing a leather jacket"
)}),
'show_status': (_IO.BOOLEAN, {'default': False, 'label_on': 'found names', 'label_off': 'no', 'tooltip': (
"Show detected string names on the node itself?"
)}),
},
'optional': {
'str_dict': _DataTypes.input_str_dict(tooltip=(
"An optional input dictionary. If it's connected, it will be expanded/updated with new strings."
)),
},
'hidden': {
'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node
},
})
class StringConstructorDictFromText:
"""
Build a dict of named sub-strings to be used later in string formatting (text construction).
"""
NODE_NAME = 'StringConstructorDictFromText'
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(), )
# OUTPUT_TOOLTIPS = tuple()
@classmethod
def INPUT_TYPES(cls):
return _input_types
def main(
self, cleanup: bool, strings: str, show_status: bool,
str_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)
+52
View File
@@ -0,0 +1,52 @@
# encoding: utf-8
"""
"""
import typing as _t
from inspect import cleandoc as _cleandoc
from frozendict import deepfreeze as _deepfreeze
from . import _meta
from .docstring_formatter import format_docstring as _format_docstring
from .enums import DataTypes as _DataTypes
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."),
},
# 'optional': {},
'hidden': {
'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node
},
})
class StringConstructorDictPreview:
"""
Show the contents of a Format-Dict.
"""
NODE_NAME = 'StringConstructorDictPreview'
CATEGORY = _meta.category
DESCRIPTION = _format_docstring(_cleandoc(__doc__))
OUTPUT_NODE = True
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, str_dict: _t.Dict[str, _t.Any],
unique_id: str = None,
):
return _preview_str_dict(str_dict, unique_id=unique_id)
+68
View File
@@ -0,0 +1,68 @@
# 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_formatter import formatter as _formatter
# A tiny optimization by reusing the same immutable dict:
_input_types = _deepfreeze({
'required': {
'template': (_IO.STRING, {'multiline': True, 'tooltip': (
"Type the text template. "
"To reference named substrings from format-dictionary, use this syntax: {substring_name}. For example:\n\n"
"score_9, score_8_up, score_7_up, {char1_short}, standing next to {char2_short},\n"
"{char1_long}\n{char2_long}"
)}),
'show_status': (_IO.BOOLEAN, {'default': True, 'label_on': 'formatted string', 'label_off': 'no', 'tooltip': (
"Show the final string constructed from the text-template and format-dictionary?"
)}),
},
'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."
)),
},
'hidden': {
'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node
},
})
class StringConstructorFormatter:
"""
Construct the formatted string from template and format-dictionary.
"""
NODE_NAME = 'StringConstructorFormatter'
CATEGORY = _meta.category
DESCRIPTION = _format_docstring(_cleandoc(__doc__))
OUTPUT_NODE = True
FUNCTION = 'main'
RETURN_TYPES = (_IO.STRING, )
RETURN_NAMES = ('string', )
# OUTPUT_TOOLTIPS = tuple()
@classmethod
def INPUT_TYPES(cls):
return _input_types
def main(
self, template: str, show_status: bool,
str_dict: _t.Dict[str, _t.Any] = None,
unique_id: str = None,
):
return _formatter(template, str_dict, show=show_status, unique_id=unique_id)
+19
View File
@@ -0,0 +1,19 @@
[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"
authors = [
{name = "Lex Darlog"}
]
license = {file = "LICENSE.md"}
readme = "README.md"
dependencies = ["frozendict"]
[project.urls]
Repository = "https://github.com/Lex-DRL/ComfyUI-BestResolution"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "lex-drl"
DisplayName = "String Constructor (Text-Formatting)"
Icon = ""
+1
View File
@@ -0,0 +1 @@
frozendict