Merge branch 'dev': v0.0.1
This commit is contained in:
@@ -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 %}
|
||||
@@ -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
@@ -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.
|
||||
@@ -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)
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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']
|
||||
@@ -0,0 +1,6 @@
|
||||
# encoding: utf-8
|
||||
"""
|
||||
Metadata-related module.
|
||||
"""
|
||||
|
||||
category: str = "String Constructor"
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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),)
|
||||
@@ -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), )
|
||||
@@ -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, )
|
||||
@@ -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 |
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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 = ""
|
||||
@@ -0,0 +1 @@
|
||||
frozendict
|
||||
Reference in New Issue
Block a user