**INITIAL RELEASE** - fork of StringConstructor

This commit is contained in:
Lex Darlog (DRL)
2026-04-21 04:02:10 -03:00
24 changed files with 496 additions and 1012 deletions
-57
View File
@@ -1,57 +0,0 @@
## `TODO`
The pack is considered feature-complete.
No other features planned (but I have nothing against improvements: feel free to [Pull Request](../../pulls)).
To do some fancy stuff with dictionaries, specialized node packs are recommended instead.
# v1.1.2
- `[fix]` [False-positive `Input-dict isn't a dict` type-check error on older python versions (3.10)](https://github.com/Lex-DRL/ComfyUI-StringConstructor/issues/4)
# v1.1.1
- Shorten tooltip for `safe_format` toggle.
# v1.1.0
- `✨New feature` Safe-formatting mode for `String Formatter`:
- When an invalid pattern included in the formatted template - instead of throwing an error, leaves this part of the template as-is. Useful for templates with curly braces in them intended to stay: JSON/CSS.
- The entire implementation of `String Formatter` node is fully reworked because of it.
# v1.0.4
- All the secondary dict-related nodes are moved to a subcategory.
- Default node name shortened: `Format-Dict` → just `Dict`.
- `✨New node` - `Extract String from Dict` - to be actually self-sufficient for the common use cases.
# v1.0.3
- [fix] Nodes made compatible with built-in preview nodes.
- Some internal refactoring/cleanup.
# v1.0.2
- `✨New node` - `Validate Format-Dict`
# v1.0.1
- `✨New feature` ❗ Recursive formatting.
- Tiny tweaks to tooltips/descriptions.
# v1.0.0
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:
- `Add String to Format-Dict`
- `Format-Dict from Text`
- `Format-Dict Preview`
- `String Formatter`
+41 -180
View File
@@ -2,204 +2,65 @@
> "Simple is better than complex." _— Zen of Python_
# `String Constructor` (Text-Formatting) nodes
... for ComfyUI.
<div align="center">
[🔄 Updates ChangeLog](CHANGELOG.md)
# `🗂️ Dict Tools` for ComfyUI
*Essential nodes to use dictionaries in ComfyUI: for smart prompt-formatting, general organization (passing a single connection instead of spaghetti), or anything else.*<br />
</div>
### TL;DR
There's already a plenty of string-formatting nodes for ComfyUI. But this node pack takes a different approach:
- Build your dictionary of available text chunks once. Pass it further as a single line (bus/pipe design).
- Easily reuse these sub-strings to build as many variations of a prompt as needed.
- It's especially handy for regional prompting (aka area composition).
- [❗ Recursive formatting support](#recursive-formatting-)
- ...
- PROFIT!
- In python, there's a built-in "dictionary" (`dict`) data type.
- It's both simple and powerful: it's a single container that lets you put many pieces of data into it, **and still access them individually** - by their unique names aka keys. For example, you can put there:
- `seed` (int)
- `denoise` value (float)
- `text` prompt (string)
- ... and even have multiple versions of them for different stages of workflow - as long as each value has its own unique key.
- Even though ComfyUI actively uses dicts under the hood, there's no built-in `DICT` data type exposed to users, nor there are any nodes to build/modify/use dicts.
- This node pack aims to fix that.
- I **highly** recommend using it together with my [String Constructor/Formatter](https://github.com/Lex-DRL/ComfyUI-StringConstructor) node and with [Basic Data Handling](https://github.com/StableLlama/ComfyUI-basic_data_handling) pack.
![screenshot1](img/screenshot1.png)
## What for?
## The main `String Formatter` node
In short:
- Advanced text formatting;
- Utilizing the well-known "bus" workflow to reduce spaghetti, while letting **you** decide what data is contained inside the bus - not the node author deciding for you;
- Any other uses for dicts - your choice!
It expects one input with a full "library" (dictionary) of all the named strings you might need, and it lets you use them in a prompt - any of them (or none at all), in any order, anywhere inside text template - using [python's string formatting syntax](https://docs.python.org/3/library/string.html#format-examples)... **Don't panic!** All the unfamiliar programming terms are explained below.
Now, specifics.
But the main point is, you don't need to fiddle around connecting a bunch of wires every time you'd like to build another prompt, compiled from the same parts. There's just a single node, single input _(with all the available text pieces contained within it)_, single text field - and that's it.
### Formatting
> [!NOTE]
> Unlike many other _(giant uber-mega)_ node packs, this one strictly adheres to the modular philosophy: it's minimal and self-sufficient. It's designed to only do string formatting and nothing else.
Originally, all these nodes were just a part of my [String Constructor pack](https://github.com/Lex-DRL/ComfyUI-StringConstructor) as "supporting" nodes. So they let you prepare a dictionary with various text chunks, and then build the actual prompt from them, easily reusing the same descriptions across the workflow. Like this:
### Dictionary
![example-graph-with-constructor](img/example-graph-with-constructor.png)
For those unfamiliar, dictionary is a special datatype, which can contain a bunch of individual values (as many as you need), each of which accessible with its own **unique** name _(key)_. For our purposes, it can be understood as a simple "search & replace" table. So, you prepare a bunch of distinct parts of the prompt, you name them and then you can reference any of them individually.
### Bus connection
For example, in the screenshot shown above, this is the dictionary we get in the end (as the `Preview` node confirms):
... but it quickly became apparent, that dicts are good not only for that - they're perfect as general-purpose "wrappers" over big sets of **any** data, no matter what is its type. So:
- You put all your seeds/prompts/denoise values/whatever (even models) into a single dictionary.
- You pass it as a **single** connection across the graph.
- Yes, if you wish, also using any other spaghetti-reducing nodes - like `Set`/`Get` nodes from [KJNodes pack](https://github.com/kijai/ComfyUI-KJNodes) or any nodes from [UE pack](https://github.com/chrisgoringe/cg-use-everywhere).
- When needed, you extract these values back from the dict and connect them as usual. No special bus-understanding nodes required.
| Chunk Name (key) | Chunk Text (value) |
|------------------|--------------------------------------------|
| `model_prefix` | <pre>score_9, score_8_up, score_7_up</pre> |
| `char1_short` | <pre>1boy, blond, short hair</pre> |
| `char1_long` | <pre>1boy, smiling, blue eyes, blond, short hair,[NEW LINE HERE]<br>wearing a leather jacket, sitting on a bike</pre> |
Also, dicts are designed to be easily updated, so you could initially build one "base" dict, another "override" dict, and somewhere down the workflow you apply the latter - getting a third dict which preserves all the original values from the first one, but takes the updated values from the second one.
And then, we can do **this** with the same input dictionary:
This "base-then-override" approach lets you build your own templates containing entire **sets** of values, applied all at once with a single node when you need it. If you're familiar with Automatic1111 UI, then it's basically a "style preset" concept, but much, much, **MUCH** more flexible and powerful.
![screenshot2](img/screenshot2.png)
## Examples 🚧🏗️
> [!NOTE]
> The `BREAK` keyword shown here isn't natively supported by Comfy's `CLIP Text Encode`. But you can use [`CLIPTextEncode with BREAK syntax`](https://github.com/dfl/comfyui-clip-with-break) or [`CLIP Text Encode++` from smZNodes](https://github.com/shiimizu/ComfyUI_smZNodes) (if you want to fully switch from Comfy's to A1111's way of encoding text).
(Section under construction)
## Implementation details for programmers
### String-Formatting syntax
The exact datatype the nodes in this pack output is actually `frozendict` (not native `dict`) - to prevent accidentally mutating the same dict by multiple nodes.
The way you "paste" values from the dictionary into your text template is called "string formatting". Its syntax is very simple:
- You just put your `{key_name}` inside curly braces (no spaces between) - and voila! When the prompt template gets formatted, this pattern will be replaced with the actual sub-string from the dict.
- For your convenience, the string-formatting node itself can show the actual output.
- Also keep in mind, that while naming your sub-strings, you must follow the same restrictions python imposes on any variable names: only ASCII letters, digits and underscore are allowed + the name can't start with a digit. So:
- ✅ `valid_name`, `_other_valid_name_`, `YetAnother_ValidName___`, `name4`.
- ❌ `wrong name with spaces`, `wrong-name.with:punctuation`, `4name`.
- Don't worry, the node pack will warn you if you type an invalid name.
## Versioning scheme
### Recursive formatting <img src="img/recursive_toggle.png" height="24" />
I adopted a custom versioning scheme, which looks like a regular SemVer, but is slightly different:
- First number is internal node-API version.
- Second is major update - when it bumps, it contains breaking changes.
- Last is a minor update.
It's quite simple in principle yet the most powerful feature of the pack.
When formatting the string, you can let chunks reference each other, which unlocks immense possibilities - like building entire **HIERARCHIES** of descriptions with various granularity (tailored for different resolutions) or even [conditional string formatting](#dynamic-pattern-aka-conditional-formatting) for the most advanced users.
- You can design your dict to be intended for updating down the line. Change some keys → get a different prompt with the same template (for example, a more detailed description of a character).
- Don't forget that with recursive formatting, [template itself could be a part of the dictionary](#pattern-as-part-of-the-dictionary), too!
![recursive_screenshot_v2](img/recursive_screenshot_v2.png)
> [!WARNING]
> Remember that with great power comes great responsibility!
>
> With a sloppy use, you can create chunks that cross-reference each other in an infinite loop. The node will error out after reaching a high level of recursion (about 1k), so you're safe. But still, you've been warned!
## Helper nodes for Dictionaries
At this point it should be clear that most of the work would be done around preparing the dictionary to use.
The pack provides some utility nodes to build such dict:
- `Dict from Text` - **this node would be enough 99% of the time**. It parses a single wall of text and splits it into chunks at empty lines. The first line in each chunk is extracted as key, the rest of the chunk _(including any number of the following new lines, as long as they have some text)_ is the actual text of this chunk.
- `Add String to Dict` - similar, but adds only one entry. Useful when you need a value or a key of the dictionary entry to come as input connection from somewhere else.
- `Add ANY to Dict` similar, but for advanced formatting. It allows you to add not only a string, but literally anything (float, int, etc). The key still must follow the same restrictions.
- Any of these nodes can take another dictionary as input - then they output the extended/updated dict.
- `Extract String from Dict` - the opposite to `Add String to Dict`: extracts a single element. With these two nodes, you can extract a single string, modify it, and update the dict with the new version. Technically, the main `String Formatter` node can "extract" string, too - but this one is more compact.
- `Validate Dict` - a node that ensures that all the keys in the dictionary are named properly. Useful if you build the dictionary with nodes from other packs (see below) and want to ensure that everything is fine - before passing the dictionary down the line.
These bundled nodes should be enough to start your journey. If you need to do a more advanced stuff with dictionaries, you can look into other node packs specializing on exactly that. Good candidates are:
- [Basic Data Handling](https://github.com/StableLlama/ComfyUI-basic_data_handling)
- [Eugene's Nodes](https://github.com/JEONG-JIWOO/ComfyUI_Eugene_Nodes)
- [WAS Node Suite](https://github.com/ltdrdata/was-node-suite-comfyui). Note: this is a mega-pack, over-bloated to my taste. It has nodes to work with dictionaries, but also **A TON** of other unrelated stuff... and it's known to have dependencies conflicting with other custom nodes.
## Helper nodes for Preview
To debug the dictionary you build, there's a `Preview Dict` node.
You might also look into the built-in `Preview Any` node.
Alternatively, [Crystools](https://github.com/crystian/ComfyUI-Crystools) pack has a `🪛 Show any to JSON` node _(I highly recommend it)_, which produces a more readable output. Crystools pack isn't as bloated as WAS' one, but still, it's a multipurpose mega-pack, too.
## Advanced Topics
### Pattern as part of the dictionary
With [Recursive formatting](#recursive-formatting-), nothing stops you from putting the template itself into the dictionary, too. You can even have both your prompts there (positive/negative), and you unpack them with a couple of `String Formatter` nodes _(with their only text being something like `{pos_template}` / `{neg_template}`)_, just before KSampler.
This single possibility can **drastically** change your approach to workflow creation. Since building a prompt for a specific KSampler becomes effectively free (with a properly configured dictionary) and string-formatting/text-encoding is almost free, too - you no longer need passing spaghetti of your pre-encoded conditionings all across your workflow. Instead, you just build them in-place by simply setting a couple of "special" keys to the desired toggle-like values, and pass only your all-in-one dictionary itself.
#### A negative prompt example
![neg_recursive_example](img/neg_recursive_example.png)
Try it yourself:
```
bad_quality
(worst quality:1.2), (low quality:1.2), (normal quality:1.2), lowres
bad_anatomy_short
bad anatomy
bad_anatomy_extra
ugly, unnatural body, error
bad_anatomy_long
{bad_anatomy_short}, {bad_anatomy_extra}
bad_hands_short
bad hands
bad_hands_extra
extra finger, missing fingers
bad_hands_long
{bad_hands_short}, {bad_anatomy_long}, {bad_hands_extra}
bad_eyes_short
imperfect eyes, skewed eyes
bad_face_short
{bad_eyes_short}, unnatural face
bad_face_long
{bad_face_short}, {bad_anatomy_long}
bad_limbs_short
extra limb, missing limbs
bad_human_long
{bad_anatomy_short}, {bad_hands_short}, {bad_face_short}, {bad_anatomy_extra}, {bad_limbs_short}
watermark
signature, watermarks
neg_common
{bad_quality}, {watermark}, {bad_human_long}
```
### When you need curly braces themselves
#### Safe mode `✨New in v1.1.0`
The simplest solution - just enable `safe_format` toggle. It changes the node behavior: whenever formatter encounters any `{text inside curly braces}` that cannot be formatted _(for any reason: the dict misses this key, or it's an outright invalid pattern)_, it doesn't throw an error and instead simply leaves this part of template as-is.
It's extremely useful for code-like templates (e.g., containing JSON or CSS).
However, even for proper `{patterns}` referencing proper dict keys, the manual escaping (see below) also applies.
> [!NOTE]
> Keep in mind that any spaces inside braces immediately make this pattern invalid.
>
> Thus, if you want to have something like: `{{my_key}}` _(so, `{my_key}` to be replaced + it's wrapped into another set of braces which you want to have in the output)_, the easiest way to do it without escaping is simply enabling safe mode and adding a couple of spaces inside the braces you want to stay. So: `{ {my_key} }` - the innermost part will be replaced with the value of `my_key` item from the dict, while the outer braces will stay.
#### Manual escaping - double braces
If you want to explicitly control which braces perform formatting and which don't - to have the literal curly-brace characters inside the prompt **after** formatting, you need to [escape](https://docs.python.org/3/library/string.html#format-string-syntax) them: whenever you need one, you type it twice (`'{{'` or `'}}'`). Then, after formatting, it will turn to `'{'` or `'}'`, respectively.
Keep in mind though, that with escaping approach and [Recursive formatting](#recursive-formatting-), any `{{text}}` will become `{text}` after very first iteration, and thus on the next one, it still will be treated as a placeholder to put a value of the `text` key into.
However, this might be exactly what you want for...
### Dynamic pattern aka conditional formatting
In other words, you build a prompt, where **keys themselves** are compiled from pieces. For example:
- Your main text template has one of the following patterns somewhere inside it:
- {{character_`{active_char}`}} - if safe mode is off;
- {character_`{active_char}`} - if safe mode is on.
- You also have a string named `active_char` in your dict, which you simply set to a number.
- Also-also, you have strings named `character_1`, `character_2`, etc.
- Then, with recursive formatting, depending on just a **single** value you set the `active_char` element to, the following happens:
- At the first iteration, `{{` in the beginning and `}}` in the end are treated as escaped ones, so they're simply replaced to `{` and `}`.
- The middle `{active_char}` part, however, is treated as a real pattern to replace.
- Thus, after the first iteration, `{{character_{active_char}}}` becomes `{character_1}`...
- ... and in the next iteration, the actual value of `character_1` string is placed here instead.
Clearly, to take advantage of this approach, you need to build your dictionary very cautiously and precisely. But if you do so, you effectively get a way of building entire **logic trees**, represented as data (not as code), all self-contained inside a single dictionary, but still letting you generate a **completely** different prompt by changing values of one or two such "switcher" strings.
At this point, your imagination is your only limit.
### Implementation details for programmers
Internally in the main `String Formatter` node, just a built-in `str.format_map()` is called with keyword arguments from the passed Format-Dict, which is literally just a dict _(and it's expected to have only string keys)_.
So any "complex" formatting patterns are available, too (like `{float_value:.3f}`). `Add ANY to Format-Dict` node is there for exactly that.
## [TODO.md](TODO.md)
+6
View File
@@ -0,0 +1,6 @@
# `TODO`
- Auto-parse built-in types in `DictFromText`.
- Add/extract nodes for all the built-in types in Comfy.
- Bulk key-rename nodes: add/remove prefix, replace.
- Support non-strings as keys.
+12 -27
View File
@@ -2,33 +2,18 @@
"""
"""
import typing as _t
from comfy_api.latest import ComfyExtension as _ComfyExtension, io as _io
from .node_dict_add_any import StringConstructorDictAddAny
from .node_dict_add_string import StringConstructorDictAddString
from .node_dict_key_extract import StringConstructorDictExtractString
from .node_dict_from_text import StringConstructorDictFromText
from .node_dict_preview import StringConstructorDictPreview
from .node_formatter import StringConstructorFormatter
from .node_validate_keys import StringConstructorValidateKeys
from .node_dict_from_text import *
from .nodes_dict import *
NODE_CLASS_MAPPINGS: _t.Dict[str, type] = {
'StringConstructorDictAddAny': StringConstructorDictAddAny,
'StringConstructorDictAddString': StringConstructorDictAddString,
'StringConstructorDictExtractString': StringConstructorDictExtractString,
'StringConstructorDictFromText': StringConstructorDictFromText,
'StringConstructorDictPreview': StringConstructorDictPreview,
'StringConstructorFormatter': StringConstructorFormatter,
'StringConstructorValidateKeys': StringConstructorValidateKeys,
}
NODE_DISPLAY_NAME_MAPPINGS: _t.Dict[str, str] = {
'StringConstructorDictAddAny': "Add ANY to Dict",
'StringConstructorDictAddString': "Add String to Dict",
'StringConstructorDictExtractString': "Extract String from Dict",
'StringConstructorDictFromText': "Dict from Text",
'StringConstructorDictPreview': "Preview Dict",
'StringConstructorFormatter': "String Formatter",
'StringConstructorValidateKeys': "Validate Dict",
}
class DictToolsExtension(_ComfyExtension):
async def get_node_list(self) -> list[type[_io.ComfyNode]]:
return [
DictFromText, DictFromTextOld1,
DictAddAny, DictAddString, DictExtractString,
DictAddAnyOld1, DictAddStringOld1, DictExtractStringOld1
]
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
async def comfy_entrypoint() -> _ComfyExtension:
return DictToolsExtension()
+8
View File
@@ -0,0 +1,8 @@
# encoding: utf-8
"""
Metadata-related module.
"""
category: str = "🗂️ Dict Tools"
category_old: str = f"{category}/_old"
pack_id_suffix: str = " [Dict Tools]"
+10
View File
@@ -0,0 +1,10 @@
# encoding: utf-8
"""
"""
import typing as _t
from typing import Any as _A, Optional as _O, Union as _U, TypeVar as _TypeVar
T = _TypeVar('T')
T2 = _TypeVar('T2')
FormatDict = _TypeVar('FormatDict', bound=_U[_t.Dict[str, _A], _t.Mapping[str, _A]])
+2 -15
View File
@@ -1,6 +1,6 @@
# encoding: utf-8
"""
Code shared between various `funcs_*` modules.
Shared code to deal with dicts.
"""
import typing as _t
@@ -9,20 +9,7 @@ import re as _re
from frozendict import deepfreeze as _deepfreeze, frozendict as _frozendict
from server import PromptServer as _PromptServer
from .enums import T as _T, T2 as _T2
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)
from .__typing import T as _T, T2 as _T2
_re_valid_key_match = _re.compile("[a-zA-Z_][a-zA-Z_0-9]*$").match
+85
View File
@@ -0,0 +1,85 @@
# encoding: utf-8
"""
"""
from dataclasses import fields as __dataclass_fields
from comfy_api.latest import io as _io
from .__meta import category_old as _category_old
from .__typing import _t, T as _T
_DICT = _io.Custom("DICT")
_DICT_INPUT_OPTIONAL = _DICT.Input(
'dict',
optional=True,
tooltip="An (optional) Dictionary to work with."
)
_DICT_OUTPUT = _DICT.Output(
'DICT',
display_name='dict'
)
_T_Input = _t.TypeVar('T_Input', bound=_io.Input)
def __dataclass_fields_dict(obj) -> _t.Dict[str, _t.Any]:
"""Return all the field values from a dataclass - as dict. No deep-copy."""
field_names = (field.name for field in __dataclass_fields(obj))
return {
attr: getattr(obj, attr)
for attr in field_names
}
def _dataclass_override(obj: _T, **overrides) -> _T:
"""Copy a dataclass instance with optional overrides."""
obj_dict = __dataclass_fields_dict(obj)
obj_dict.update(overrides)
obj_type = type(obj)
return obj_type(**obj_dict)
def _input_override(input_obj: _T_Input, **overrides) -> _T_Input:
"""Copy an ``io.Input`` instance with optional overrides."""
new_id = input_obj.id
if 'id' in overrides:
new_id = overrides.pop('id')
input_args = input_obj.as_dict()
if 'id' in input_args:
del input_args['id']
input_args.update(overrides)
input_type: _t.Type[_T_Input] = type(input_obj)
return input_type(new_id, **input_args)
def _schema_old_name(schema: _io.Schema, node_id: str, **overrides):
"""Build a deprecated schema for old node-ID."""
schema_dict = __dataclass_fields_dict(schema)
if 'node_id' in schema_dict:
del schema_dict['node_id']
name_old = schema_dict.pop('display_name')
schema_dict.update(dict(
category=_category_old,
display_name=f"{name_old} [OLD]",
is_deprecated=True,
is_dev_only=True,
))
schema_dict.update(overrides)
return _io.Schema(node_id, **schema_dict)
# noinspection PyAbstractClass
class _BaseNode(_io.ComfyNode):
"""Shared base class for all the nodes in pack.
Stores schema in internal class attribute.
"""
_schema: _io.Schema = None
@classmethod
def define_schema(cls) -> _io.Schema:
return cls._schema
-7
View File
@@ -1,7 +0,0 @@
# encoding: utf-8
"""
Metadata-related module.
"""
category: str = "String Constructor"
category_dict: str = f"{category}/Format-Dict"
-39
View File
@@ -1,39 +0,0 @@
# 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')
T2 = _t.TypeVar('T2')
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."""
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]:
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_dict(cls, *_dicts: _t.Dict[str, T], **kwargs: T) -> _t.Tuple[_t.Union['DataTypes', str], _t.Dict[str, T]]:
return str(cls.DICT), cls.__custom_input_type_dict(_dicts, kwargs)

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

-65
View File
@@ -1,65 +0,0 @@
# 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_common import _new_dict_with_updated_key
from .node_dict_add_string import _input_types as _input_types_str
_dict = dict
# A tiny optimization by reusing the same immutable dict:
_input_types = _deepfreeze({
'required': {
'name': (_IO.STRING, {'tooltip': (
"Name (key) 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': {
'dict': _input_types_str['optional']['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_dict
DESCRIPTION = _format_docstring(_cleandoc(__doc__))
FUNCTION = 'main'
RETURN_TYPES = (str(_DataTypes.DICT), )
RETURN_NAMES = (_DataTypes.DICT.lower(), )
# OUTPUT_TOOLTIPS = tuple()
@classmethod
def INPUT_TYPES(cls):
return _input_types
@staticmethod
def main(
name: str, dict: _t.Dict[str, _t.Any] = None, value: _t.Any = None,
# unique_id: str = None,
) -> _t.Tuple[_t.Dict[str, _t.Any]]:
"""Update/append an item of any type to the dict."""
if value is None:
if dict is None:
dict = _dict()
return (dict, ) # No need to create another dict instance if we add nothing
return (_new_dict_with_updated_key(dict, name, value), )
-67
View File
@@ -1,67 +0,0 @@
# 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_common import _new_dict_with_updated_key, _T
# A tiny optimization by reusing the same immutable dict:
_input_types = _deepfreeze({
'required': {
'name': (_IO.STRING, {'tooltip': (
"Name (key) 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': '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 add into the dict."}),
},
'optional': {
'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
# },
})
class StringConstructorDictAddString:
"""Add/update a string to the Format-Dict."""
NODE_NAME = 'StringConstructorDictAddString'
CATEGORY = _meta.category_dict
DESCRIPTION = _format_docstring(_cleandoc(__doc__))
FUNCTION = 'main'
RETURN_TYPES = (str(_DataTypes.DICT), )
RETURN_NAMES = (_DataTypes.DICT.lower(), )
# OUTPUT_TOOLTIPS = tuple()
@classmethod
def INPUT_TYPES(cls):
return _input_types
@staticmethod
def main(
name: str, cleanup: bool, string: str,
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."""
string = '' if string is None else str(string)
if cleanup:
string = '\n'.join(
x.strip() for x in string.splitlines()
)
return (_new_dict_with_updated_key(dict, name, string), )
+94 -64
View File
@@ -1,21 +1,26 @@
# encoding: utf-8
"""
"""
import typing as _t
"""Code for the ``Dict from Text`` node."""
from inspect import cleandoc as _cleandoc
from frozendict import frozendict as _frozendict, deepfreeze as _deepfreeze
from comfy_api.latest import io as _io, ui as _ui
from comfy.comfy_types.node_typing import IO as _IO
from . import _meta
from .__meta import (
category as _category,
pack_id_suffix as _pack_id
)
from .__typing import _A, _U, _O, _t, T as _T, FormatDict as _FormatDict
from ._dict_funcs import _new_updated_dict
from ._io_custom import (
_BaseNode,
_DICT_INPUT_OPTIONAL, _DICT_OUTPUT,
_input_override, _schema_old_name
)
from .docstring_formatter import format_docstring as _format_docstring
from .enums import DataTypes as _DataTypes
from .funcs_common import _show_text_on_node, _new_updated_dict, _T
from .node_dict_add_string import _input_types as _input_types_str
_dict = dict
# ----------------------------------------------------------
def _return_line_raw(line_raw: str, line_stripped:str) -> str:
return line_raw
@@ -26,7 +31,7 @@ def _return_line_stripped(line_raw: str, line_stripped:str) -> str:
def _parsed_kv_pairs_gen(multiline_str: str, strip_lines=True):
"""
"""Parse the "dict text" into sequence of key-value pairs:
Given a multiline string, extract keywords (first non-empty line in each chunk)
and their substrings (all the following non-empty lines).
"""
@@ -62,68 +67,93 @@ def _parsed_kv_pairs_gen(multiline_str: str, strip_lines=True):
if cur_chunk:
yield dump_chunk()
# --------------------------------------
# ----------------------------------------------------------
_dict = dict
_input_types = _deepfreeze({
'required': {
'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': (
"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,\nwearing a leather jacket, sitting on a bike"
)}),
'show_status': (_IO.BOOLEAN, {'default': False, 'label_on': 'detected names', 'label_off': 'no', 'tooltip': (
"Show detected string names on the node itself?"
)}),
},
'optional': {
'dict': _input_types_str['optional']['dict'],
},
'hidden': {
'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node
},
})
class StringConstructorDictFromText:
class DictFromText(_BaseNode):
"""
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__))
_schema = _io.Schema(
node_id=f'DictFromText{_pack_id}',
display_name='Dict from Text',
category=_category,
description=_format_docstring(_cleandoc(__doc__)),
inputs=[
_io.Boolean.Input(
'cleanup',
tooltip="When enabled, each line in each sub-string is stripped from any spaces at its start and end.",
default=True,
label_on='leading/trailing spaces',
label_off='no',
),
_io.String.Input(
'dict_text', display_name='dict-items text',
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,\nwearing a leather jacket, sitting on a bike"
),
multiline=True,
),
_io.Boolean.Input(
'show_status',
tooltip="Show detected string names on the node itself?",
default=False,
label_on='detected keys',
label_off='no',
),
OUTPUT_NODE = True # Just to show the status message even if not connected to anything
FUNCTION = 'main'
RETURN_TYPES = (str(_DataTypes.DICT), )
RETURN_NAMES = (_DataTypes.DICT.lower(), )
# OUTPUT_TOOLTIPS = tuple()
_DICT_INPUT_OPTIONAL,
],
outputs=[_DICT_OUTPUT],
# hidden=[_io.Hidden.unique_id],
is_output_node=True, # Just to show the status message even if not connected to anything
)
@classmethod
def INPUT_TYPES(cls):
return _input_types
@staticmethod
def main(
cleanup: bool, strings: str, show_status: bool = True,
dict: _t.Dict[str, _T] = None, unique_id: str = None,
) -> _t.Tuple[_t.Dict[str, _t.Union[_T, str]]]:
new_dict = {k: v for k, v in _parsed_kv_pairs_gen(strings, strip_lines=cleanup)}
def execute(cls,
cleanup: bool, dict_text: str, show_status: bool = False,
dict: _O[_FormatDict] = None
) -> _io.NodeOutput:
parsed_items = _parsed_kv_pairs_gen(dict_text, strip_lines=cleanup)
new_dict = {k: v for k, v in parsed_items}
if not new_dict:
if dict is None:
dict = _dict()
return (dict, ) # No need to create another dict instance if we add nothing
# No need to create another dict instance if we add nothing:
out_dict = _dict() if dict is None else dict
out_ui = '' if show_status else None
return _io.NodeOutput(out_dict, ui=out_ui)
out_dict = _new_updated_dict(dict, new_dict)
# These two ↑↓ must be in this specific order: `_new_updated_dict()` also checks both dicts
if show_status and unique_id:
# These two ↑↓ must be in this specific order:
# `_new_updated_dict()` also checks both dicts and might throw an error
out_ui = None
if show_status:
status_text = ','.join(new_dict.keys()) if new_dict else ''
_show_text_on_node(status_text, unique_id)
out_ui = _ui.PreviewText(status_text)
return (out_dict, )
return _io.NodeOutput(out_dict, ui=out_ui)
# ==========================================================
# Deprecated node with old ID (for backwards compatibility)
class DictFromTextOld1(_BaseNode):
# noinspection PyProtectedMember
_schema = _schema_old_name(
DictFromText._schema,
'StringConstructorDictFromText',
inputs=[
_input_override(x, id='strings') if x.id == 'dict_text' else x
for x in DictFromText._schema.inputs
],
)
@classmethod
def execute(cls,
cleanup: bool, strings: str, show_status: bool = False,
dict: _O[_FormatDict] = None
) -> _io.NodeOutput:
return DictFromText.execute(
cleanup=cleanup, dict_text=strings, show_status=show_status,
dict=dict
)
-74
View File
@@ -1,74 +0,0 @@
# 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_common import _show_text_on_node, _new_dict_with_updated_key, _T
_input_types = _deepfreeze({
'required': {
'name': (_IO.STRING, {'tooltip': (
"Name (key) of a string to extract from dictionary.\n"
"If the element with such key isn't a string, it will be turned to one.\n"
"If no such key exists in the dict, an empty string returned."
)}),
'show_status': (_IO.BOOLEAN, {'default': False, 'label_on': 'value', 'label_off': 'no', 'tooltip': (
"Show the extracted string on the node itself?"
)}),
},
'optional': {
'dict': _DataTypes.input_dict(tooltip="The dictionary to extract the element from."),
},
'hidden': {
'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node
},
})
class StringConstructorDictExtractString:
"""Extract a single string from the Format-Dict."""
NODE_NAME = 'StringConstructorDictExtractString'
CATEGORY = _meta.category_dict
DESCRIPTION = _format_docstring(_cleandoc(__doc__))
FUNCTION = 'main'
RETURN_TYPES = (_IO.STRING, )
RETURN_NAMES = ('string', )
# OUTPUT_TOOLTIPS = tuple()
@classmethod
def INPUT_TYPES(cls):
return _input_types
@staticmethod
def main(
name: str, show_status: bool = False, dict: _t.Dict[str, _T] = None,
unique_id: str = None,
) -> _t.Tuple[str]:
"""Extract a single string from the Format-Dict."""
# noinspection PyBroadException
try:
string: str = dict.get(name, '')
except Exception:
string = ''
if string is None:
string = ''
if not isinstance(string, str):
string = repr(string)
if show_status and unique_id:
_show_text_on_node(string, unique_id)
return (string, )
-96
View File
@@ -1,96 +0,0 @@
# encoding: utf-8
"""
"""
import typing as _t
from inspect import cleandoc as _cleandoc
from pprint import pformat as _pformat
from frozendict import deepfreeze as _deepfreeze, frozendict as _frozendict
from . import _meta
from .docstring_formatter import format_docstring as _format_docstring
from .enums import DataTypes as _DataTypes
from .funcs_common import _show_text_on_node, _verify_input_dict_into_new
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)
# In py3.10, frozendict isn't a dict, but is a `typing.Mapping`.
# So, this many types to check against:
if isinstance(v, (dict, _frozendict, _t.Mapping))
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_into_new(input_dict))
parts: _t.List[str] = [
_preview_single_entry(k, v) for k, v in input_dict.items()
]
return '\n\n'.join(parts)
# --------------------------------------
_dict = dict
_input_types = _deepfreeze({
'required': {},
'optional': {
'dict': _DataTypes.input_dict(tooltip="A Format-Dictionary."),
},
'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_dict
DESCRIPTION = _format_docstring(_cleandoc(__doc__))
OUTPUT_NODE = True
FUNCTION = 'main'
RETURN_TYPES = (str(_DataTypes.DICT), )
RETURN_NAMES = (_DataTypes.DICT.lower(), )
# OUTPUT_TOOLTIPS = tuple()
@classmethod
def INPUT_TYPES(cls):
return _input_types
@staticmethod
def main(
dict: _t.Dict[str, _t.Any] = None,
unique_id: str = None,
):
if dict is None:
dict = _dict()
if unique_id:
status_text = _preview_message(dict)
_show_text_on_node(status_text, unique_id)
return (dict, )
-253
View File
@@ -1,253 +0,0 @@
# encoding: utf-8
"""
Code for ``StringConstructorFormatter`` node.
"""
import typing as _t
from dataclasses import dataclass as _dataclass, field as _field
from inspect import cleandoc as _cleandoc
import re as _re
import sys as _sys
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_common import _show_text_on_node, _verify_input_dict
_RECURSION_LIMIT = max(int(_sys.getrecursionlimit()), 1) # You can externally monkey-patch it... but if it blows up, your fault 🤷🏻‍♂️single
__dataclass_slots_args = dict() if _sys.version_info < (3, 10) else dict(slots=True)
_re_formatting_keyword_match = _re.compile( # Pre-compiled regex match to extract ``{keyword}`` patterns
r'(?P<prefix>.*?)'
r'(?P<open_brackets>\{+)'
r'(?P<inside_brackets>[^{}]+)'
r'(?P<closed_brackets>\}+)'
r'(?P<suffix>[^{}].*)?$',
# flags=_re.DOTALL | _re.IGNORECASE,
flags = _re.DOTALL, # We need dot to match new lines, too
).match
@_dataclass(**__dataclass_slots_args)
class _Formatter:
"""
A callable (function-like) class, which does the actual formatting, while respecting all the options.
It's made as a class to split the formatting into two stages:
- First, the instance is properly initialized with the shared arguments (the methods to call are conditionally assigned depending on options);
- Then, the actual instance is treated as a function - it needs to be called with the formatted string as the only argument.
It's done this way to avoid extra conditions in the loop + to organize the convoluted mess of intertwined functions
into a more readable code.
"""
format_dict: _t.Optional[_t.Dict[str, _t.Any]] = None
recursive: bool = False
safe: bool = True
show_status: bool = True
unique_node_id: str = None
__format_single: _t.Callable[[str], str] = _field(init=False, repr=False, compare=False, default=lambda x: x)
_format: _t.Callable[[str], str] = _field(init=False, repr=False, compare=False, default=lambda x: x)
def __post_init__(self): # called by dataclass init
if self.format_dict is None:
self.format_dict = dict()
format_dict = self.format_dict
_verify_input_dict(format_dict)
self.__format_single = self.__format_single_safe if self.safe else self.__format_single_unsafe
if format_dict:
self._format = self.__format_recursive if self.recursive else self.__format_single
else:
self._format = self.__dummy_return_intact
@staticmethod
def __dummy_return_intact(template: str) -> str:
return template
def __format_single_unsafe(self, template: str):
return template.format_map(self.format_dict)
def __format_single_safe_parts_gen(self, template: str) -> _t.Generator[str, None, None]:
"""
EAFP: https://docs.python.org/3/glossary.html#term-EAFP
Instead of pre-escaping the whole template
(which would require basically re-implementing the entire format-parsing logic),
let's extract individual formatted pieces, actually try formatting them one by one,
and return anything that cannot be formatted as-is - without any processing at all.
This generator returns such pieces - formatted or intact.
"""
format_dict = self.format_dict
to_piece_template = '{{{}}}'.format
suffix: str = template
while suffix:
match = _re_formatting_keyword_match(suffix)
if not match:
break
prefix = match.group('prefix')
open_brackets = match.group('open_brackets')
inside_brackets = match.group('inside_brackets')
closed_brackets = match.group('closed_brackets')
suffix = match.group('suffix')
if prefix:
yield prefix
piece_template = to_piece_template(inside_brackets)
# noinspection PyBroadException
try:
formatted_piece = piece_template.format_map(format_dict)
except Exception:
# If, for ANY reason, we're unable to format the piece, return the template piece intact:
yield open_brackets
yield inside_brackets
yield closed_brackets
continue
# The key is found. Treat the piece as the actual formatting pattern.
# Formatting "eats" one set of brackets either way:
open_brackets = open_brackets[:-1]
closed_brackets = closed_brackets[:-1]
# Now, even though we've succeeded, the template might've been pre-escaped.
if open_brackets and closed_brackets:
# The keyword is already pre-escaped. Return it intact:
formatted_piece = inside_brackets
yield open_brackets
yield formatted_piece
yield closed_brackets
if suffix:
yield suffix
def __format_single_safe(self, template: str) -> str:
"""
Format the pattern (single iteration of formatting) in the safe mode.
Safe mode preserves any unknown ``{text patterns}`` inside curly brackets if they cannot be formatted.
Correctly handles any formatting patterns natively supported by python
(even the most fancy ones, involving ':', '!', attribute or index access, etc.).
Useful when JSON/CSS-like code is in the formatted template.
"""
return ''.join(self.__format_single_safe_parts_gen(template))
def __format_recursive(self, template: str) -> str:
"""
It's not actually recursive - because, you know, any recursion could be turned into iteration,
and good boys do that. 😊
"""
assert isinstance(_RECURSION_LIMIT, int) and _RECURSION_LIMIT > 0
format_single_func = self.__format_single
prev: str = ''
new: str = template
for i in range(_RECURSION_LIMIT):
if prev == new:
return new
prev = new
new = format_single_func(new)
msg = (
f"Recursion limit ({_RECURSION_LIMIT}) reached on attempt to format a string: {template!r}\n"
f"Last two formatting attempts:\n{prev!r}\n{new!r}"
)
if self.show_status and self.unique_node_id:
_show_text_on_node(msg, self.unique_node_id)
raise RecursionError(msg)
# noinspection PyUnreachableCode
return '' # just to be extra-safe, if RecursionError is treated as warning
def __call__(self, template: str) -> str:
if not isinstance(template, str):
raise TypeError(f"Not a string: {template!r}")
out_text = self._format(template) if template else ''
if self.show_status and self.unique_node_id:
_show_text_on_node(out_text, self.unique_node_id)
return out_text
# --------------------------------------
_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}"
)}),
'recursive_format': (_IO.BOOLEAN, {'default': False, 'label_on': '❗ yes', 'label_off': 'no', 'tooltip': (
"Do recursive format - i.e., allow the chunks from the dictionary to reference other chunks."
)}),
'safe_format': (_IO.BOOLEAN, {'default': True, 'label_on': 'yes', 'label_off': 'no', 'tooltip': (
"If template contains an invalid {text pattern} which can't be formatted - leave it as-is "
"(instead of throwing an error).\n"
"Safe mode is recommended for templates with JSON, CSS, or other literal curly brackets."
)}),
'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': {
'dict': _DataTypes.input_dict(tooltip=( # It's not actually optional, but is here since there's no dict-widget
"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
@staticmethod
def main(
template: str,
recursive_format: bool = False,
safe_format: bool = True,
show_status: bool = False,
dict: _t.Dict[str, _t.Any] = None, #actually, required - but it's here to keep the declared params order
unique_id: str = None
) -> _t.Tuple[str]:
formatter = _Formatter(
format_dict=dict,
recursive=recursive_format, safe=safe_format,
show_status=show_status, unique_node_id=unique_id,
)
out_text = formatter(template)
return (out_text, )
-61
View File
@@ -1,61 +0,0 @@
# 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_common import _verify_input_dict, _show_text_on_node, _T
_input_types = _deepfreeze({
'required': {
'dict': _DataTypes.input_dict(),
},
# 'optional': {},
# 'hidden': {
# 'unique_id': 'UNIQUE_ID', # used for text display at the bottom of the node
# },
})
class StringConstructorValidateKeys:
"""
Verify the dict to have string-formatting-compatible keys. No need to add it before "String Formatter" node,
but it might be handy to verify the dict right after it's created.
"""
NODE_NAME = 'StringConstructorValidateKeys'
CATEGORY = _meta.category_dict
DESCRIPTION = _format_docstring(_cleandoc(__doc__))
OUTPUT_NODE = True
FUNCTION = 'main'
RETURN_TYPES = (str(_DataTypes.DICT), )
RETURN_NAMES = (_DataTypes.DICT.lower(), )
# OUTPUT_TOOLTIPS = tuple()
@classmethod
def INPUT_TYPES(cls):
return _input_types
@staticmethod
def main(
dict: _t.Dict[str, _T],
# unique_id: str = None,
) -> _t.Tuple[_t.Dict[str, _T]]:
# try:
# _verify_input_dict(dict, error_if_none=True)
# except Exception:
# _show_text_on_node('❌', unique_id)
# raise
# return (Null, )
# _show_text_on_node('✅', unique_id)
_verify_input_dict(dict, error_if_none=True)
return (dict, )
+231
View File
@@ -0,0 +1,231 @@
# encoding: utf-8
"""The main module for simple dict nodes."""
from inspect import cleandoc as _cleandoc
from frozendict import frozendict as _frozendict
from comfy_api.latest import io as _io, ui as _ui
from .__meta import (
category as _category,
pack_id_suffix as _pack_id
)
from .__typing import _A, _U, _O, _t, T as _T, FormatDict as _FormatDict
from ._dict_funcs import _new_dict_with_updated_key
from ._io_custom import (
_BaseNode,
_DICT_INPUT_OPTIONAL, _DICT_OUTPUT,
_input_override, _schema_old_name
)
from .docstring_formatter import format_docstring as _format_docstring
_dict = dict
# ----------------------------------------------------------
class DictAddAny(_BaseNode):
"""Add/update any-type item to a Format-Dict."""
_schema = _io.Schema(
node_id=f'DictAddAny{_pack_id}',
display_name='Add ANY to Dict',
category=_category,
description=_format_docstring(_cleandoc(__doc__)),
inputs=[
_io.String.Input(
'name',
tooltip=(
"Name (key) of the item inserted into the dict. "
"It must comprise only of latin letters, digits and underscores + it can't start with a digit."
),
),
_DICT_INPUT_OPTIONAL,
_io.AnyType.Input(
'value',
optional=True,
tooltip="The actual any-type item to add into the dict."
),
],
outputs=[_DICT_OUTPUT],
)
@classmethod
def execute(cls,
name: str, dict: _O[_FormatDict] = None, value: _A = None
) -> _io.NodeOutput:
"""Update/append an item of any type to the dict."""
if value is None:
if dict is None:
dict = _frozendict()
# No need to create another dict instance if we add nothing:
result = dict
else:
result = _new_dict_with_updated_key(dict, name, value)
return _io.NodeOutput(result)
# ----------------------------------------------------------
class DictAddString(_BaseNode):
"""Add/update a string to a Format-Dict."""
_schema = _io.Schema(
node_id=f'DictAddString{_pack_id}',
display_name='Add String to Dict',
category=_category,
description=_format_docstring(_cleandoc(__doc__)),
inputs=[
_io.String.Input(
'name',
tooltip=(
"Name (key) of the string inserted into the dict. "
"It must comprise only of latin letters, digits and underscores + it can't start with a digit."
),
),
_io.Boolean.Input(
'cleanup',
tooltip=(
"When enabled, each line in the sub-string is stripped "
"from any spaces at its start and end."
),
default=True,
label_on='leading/trailing spaces',
label_off='no',
),
_io.String.Input(
'string',
tooltip="The actual string to add into the dict.",
multiline=True,
),
_DICT_INPUT_OPTIONAL,
],
outputs=[_DICT_OUTPUT],
)
@classmethod
def execute(cls,
name: str, cleanup: bool, string: str,
dict: _O[_FormatDict] = None,
) -> _io.NodeOutput:
"""Update/append a string to the dict."""
string = '' if string is None else str(string)
if cleanup:
string = '\n'.join(
x.strip() for x in string.splitlines()
)
result= _new_dict_with_updated_key(dict, name, string)
return _io.NodeOutput(result)
# ----------------------------------------------------------
class DictExtractString(_BaseNode):
"""Extract a single string from a Format-Dict."""
_schema = _io.Schema(
node_id=f'DictExtractString{_pack_id}',
display_name='Extract String from Dict',
category=_category,
description=_format_docstring(_cleandoc(__doc__)),
inputs=[
_io.Boolean.Input(
'cleanup_key', display_name='cleanup',
tooltip=(
"Automatically remove leading/trailing spaces and newlines from the key."
),
default=True,
label_on='leading/trailing spaces',
label_off='no',
),
_io.String.Input(
'key',
tooltip=(
"Key (unique name) of a string to extract from dictionary.\n"
"If the element with such key isn't a string, it will be turned to one.\n"
"If no such key exists in the dict, an empty string returned."
),
),
_io.Boolean.Input(
'show_status',
tooltip="Show the extracted string on the node itself?",
default=False,
label_on='value',
label_off='no',
),
_DICT_INPUT_OPTIONAL,
],
outputs=[_io.String.Output('string')],
# hidden=[_io.Hidden.unique_id],
)
@classmethod
def execute(cls,
cleanup_key: bool, key: str, show_status: bool = False,
dict: _O[_FormatDict] = None
) -> _io.NodeOutput:
"""Extract a single string from the Format-Dict."""
if cleanup_key:
for line in key.split():
line = line.strip() # just in case \r is left
if line:
key = line
break
# The passed dict might not actually be a dict but ANY mapping,
# which doesn't have a `get()` method.
# So direct key access is better:
string: str = ''
# noinspection PyBroadException
try:
string = dict[key]
except Exception:
pass
if string is None:
string = ''
elif not isinstance(string, str):
string = repr(string)
return _io.NodeOutput(
string,
ui=_ui.PreviewText(string) if show_status else None
)
# ==========================================================
# Deprecated nodes with old IDs (for backwards compatibility)
class DictAddAnyOld1(DictAddAny):
_schema = _schema_old_name(
DictAddAny._schema,
'StringConstructorDictAddAny'
)
class DictAddStringOld1(DictAddString):
_schema = _schema_old_name(
DictAddString._schema,
'StringConstructorDictAddString'
)
class DictExtractStringOld1(_BaseNode):
# noinspection PyProtectedMember
_schema = _schema_old_name(
DictExtractString._schema,
'StringConstructorDictExtractString',
inputs=[
_input_override(x, id='name') if x.id == 'key' else x
for x in DictExtractString._schema.inputs
if x.id != 'cleanup_key'
],
)
@classmethod
def execute(cls,
name: str, show_status: bool = False,
dict: _O[_FormatDict] = None
) -> _io.NodeOutput:
return DictExtractString.execute(
cleanup_key=False, key=name, show_status=show_status, dict=dict
)
+7 -7
View File
@@ -1,7 +1,7 @@
[project]
name = "string-constructor"
version = "1.1.2"
description = "String formatting (compiling text from pieces) made for humans:\n• Turn parts of prompt into a dictionary - to share them all across the entire workflow as a single connection.\n• Compose your prompt in-place from these snippets and regular text - with just one text field.\n• Easily update the dictionary down the line - get different prompts from the same template.\n• Reference text chunks within each other to build complex hierarchies of self-updating prompts.\n• A true time-saver for regional prompting (aka area composition)."
name = "dict_tools"
version = "3.1.0"
description = "Essential nodes to use dictionaries in ComfyUI: for smart prompt-formatting, general organization (passing a single connection instead of spaghetti), or anything else."
license = {file = "LICENSE.md"}
readme = "README.md"
authors = [
@@ -15,13 +15,13 @@ classifiers = [
]
[project.urls]
Repository = "https://github.com/Lex-DRL/ComfyUI-StringConstructor"
"Feedback/Bugs/Feature Requests" = "https://github.com/Lex-DRL/ComfyUI-StringConstructor/issues"
Repository = "https://github.com/Lex-DRL/ComfyUI-DictTools"
"Feedback/Bugs/Feature Requests" = "https://github.com/Lex-DRL/ComfyUI-DictTools/issues"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "lex-drl"
DisplayName = "String Constructor (Text-Formatting)"
DisplayName = "🗂️ Dict Tools"
# Icon = "https://raw.githubusercontent.com/username/repo/main/icon.png"
# Banner = "https://raw.githubusercontent.com/username/repo/main/banner.png"
# requires-comfyui = ">=0.3.51" # Node schema v3 released # TODO
requires-comfyui = ">=0.18.0" # Tested with somewhat-stable Node schema v3