Compare commits

...
Author SHA1 Message Date
asagi4 b24d93b778 v3.0.0-beta.9 2026-08-22 18:56:51 +03:00
asagi4 c8f925c4ea Lazify NODE helper 2026-08-22 18:53:02 +03:00
asagi4 1a733e71af Make NODE helper autogrow 2026-08-22 18:52:58 +03:00
asagi4 e36eff4356 Note whitespace change in README 2026-08-20 16:08:09 +03:00
asagi4 c6137ddc49 Strip whitespace from scheduled prompts by default 2026-08-20 16:04:01 +03:00
asagi4 64417230a3 SEGs need to be expanded *before* NODE is evaluated 2026-08-19 23:07:12 +03:00
asagi4 0a2ceb94e9 doc: Note caveat about missing functionality when using NODE 2026-08-19 14:02:21 +03:00
asagi4 6e3fce9dcb v3.0.0-beta.8 2026-08-19 13:42:56 +03:00
asagi4 b581cf7f24 NODE: Fix SEG expansion
See: #150
2026-08-19 13:33:46 +03:00
asagi4 d7a992b96d NODE: Test for SEG expansion 2026-08-19 13:32:04 +03:00
asagi4 19123570e1 Fix parsing of < and > in schedules, see #149 2026-08-16 17:17:38 +03:00
asagi4 e6bb57cd25 Formatting 2026-08-14 18:13:49 +03:00
10 changed files with 336 additions and 415 deletions
+3
View File
@@ -11,6 +11,9 @@ A `Basic Text to Image` template is included with the extension, and can be load
> The parser was rewritten using parsy. It is intended to have the same behaviour as the old parser, but is **significantly** faster.
> Please report any bugs or incompatibilities you find.
## Notable changes
- `PC: Schedule Prompt` now strips surrounding whitespace by default, which may change some prompts. Add `NOSTRIP()` to your prompt to restore previous behaviour.
## What can it do?
+3
View File
@@ -2,6 +2,9 @@
The `NODE` function allows you to use any other text encoding node within `PC: Schedule Prompt`, replacing the default `PCTextEncode` and allowing for example video model scheduling.
> [!NOTE]
> When using NODE, you lose access to *all* special syntax provided by `PCTextEncode`. Only SEGs, macros and scheduling will continue to work since those are processed at graph expansion time before the text prompt is passed into the node.
## Basic usage
Use `NODE(NodeClassName, textinputname)` in a prompt to generate a graph using any node that's compatible. The requirements are as follows:
+3
View File
@@ -14,6 +14,9 @@ Besides the syntax documented below, the [basic syntax](/doc/basic.md) and [prom
a [large::0.1] [cat|dog:0.05] [<lora:somelora:0.5:0.6>::0.5]
[in a park:in space:0.4]
```
## Note on whitespace
`PC: Schedule Prompt` will strip leading and following whitespace from the prompt automatically. If you really want whitespace in your prompt, include `NOSTRIP()` in your prompt.
## Comments and escaping
In schedules, any text on a line following a `#` is considered a comment and removed, including the `#` character.
File diff suppressed because it is too large Load Diff
+6
View File
@@ -197,8 +197,12 @@ def build_scheduled_prompts(graph, schedules, clip):
start_pct = 0.0
for end_pct, c in schedules:
p = c["prompt"]
# Need to explicitly expand SEGs here *before* NODE is processed
p = expand_segs(p)
p, classnames = get_function(p, "NODE", defaults=None)
realargs = ["PCTextEncode", "text", ""]
if len(classnames) > 1:
log.warning("You have more than one NODE call in your prompt. Only the first one will be used")
if classnames:
args = classnames[0].args[0]
if not args.strip():
@@ -208,6 +212,8 @@ def build_scheduled_prompts(graph, schedules, clip):
classname, paramname, magic_spec = realargs
node = graph.node(classname.strip())
node.set_input("clip", clip)
# We should strip extra whitespace so that people don't have to worry about functions.
p = p.replace("NOSTRIP()", "") if "NOSTRIP()" in p else p.strip()
node.set_input(paramname.strip(), p)
magic_spec.replace(r"\;", "__ESCAPED_SEMICOLON__")
extra_inputs = magic_spec.split(";") if magic_spec.strip() else []
+45 -9
View File
@@ -185,28 +185,64 @@ class PCMacroExpand(io.ComfyNode):
class PCLinkHelper(io.ComfyNode):
# a-z
NAMES = [chr(97 + i) for i in range(26)]
@classmethod
def define_schema(cls):
inputs: list = [io.String.Input("text", multiline=True)]
for x in "abcdefghijklmn":
inputs.append(io.AnyType.Input(x, optional=True, lazy=True, extra_dict={"rawLink": True}))
t1 = io.Autogrow.TemplateNames(io.AnyType.Input("link", raw_link=True), min=0, names=cls.NAMES)
t2 = io.Autogrow.TemplateNames(
io.AnyType.Input("value", lazy=True), min=0, names=[f"var{i + 1}" for i in range(50)]
)
return io.Schema(
node_id="PCNODELinkHelper",
display_name="PC: Extra argument helper for NODE",
category="promptcontrol/tools",
description="Takes in arbitrary inputs and renders them as NODE-compatible values, replacing $a -> $n with JSON link values",
description="Takes in arbitrary inputs and renders them as NODE-compatible values, replacing $a -> $z with JSON link values.",
is_experimental=True,
inputs=inputs,
inputs=[
io.Autogrow.Input("links", template=t1),
io.Autogrow.Input(
"vars",
template=t2,
),
io.String.Input(
"template",
tooltip="The variables $a to $z will be replaced in this text with their corresponding input's JSON link value",
placeholder="In this text you can refer to the input links as $a, $b etc. and the var inputs as either $var1 or $json1 etc. (the latter will be rendered through Python's json.dumps function which will cause strings to be quoted)",
multiline=True,
),
],
outputs=[io.String.Output()],
)
# This requires https://github.com/Comfy-Org/ComfyUI/pull/15103 to work properly
@classmethod
def execute(cls, text, **vars) -> io.NodeOutput:
for k in "abcdefghijklmn":
def check_lazy_status(cls, template, links, vars):
r = []
for name, (v, input_name) in vars.items():
if v is None and f"${name}" in template or v is None and f"$json{name[3:]}" in template:
r.append(input_name)
return r
@classmethod
def execute(cls, template, links, vars) -> io.NodeOutput:
text = template
for k in cls.NAMES:
v = "__EMPTY__"
if k in vars:
v = json.dumps(vars[k])
if k in links:
v = json.dumps(links[k])
text = substitute_var(text, k, v)
for i in range(50):
v = "__EMPTY__"
k = f"var{i + 1}"
if k in vars:
v = vars[k]
text = substitute_var(text, k, str(v))
if f"$json{i + 1}" in text:
v = v if v == "__EMPTY__" else json.dumps(v)
text = substitute_var(text, f"json{i + 1}", v)
return io.NodeOutput(text)
+11 -1
View File
@@ -370,7 +370,17 @@ ctlweight = seq(number, (string("@") >> number).optional(0)).sep_by(comma, min=1
loractl = (string("<loractl:") >> filename * 1 + lora_weights(ctlweight) << string(">")).combine(LoRACTL)
emb = (string("<emb:") >> filename << string(">")).map(lambda f: Text(f"embedding:{f}"))
expr = escape | comment | non_special | bracketed | emphasis.combine(combine_prompt) | lora | loractl | emb
expr = (
escape
| comment
| non_special
| bracketed
| emphasis.combine(combine_prompt)
| lora
| loractl
| emb
| char_from("<>").map(Text)
)
prompt_ = expr.at_least(1).combine(combine_prompt)
prompt.become(prompt_)
# Treat any character that isn't valid prompt syntax as just text
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-prompt-control"
description = "Nodes for prompt editing and LoRA scheduling, advanced regional prompting (including attention masking) and advanced prompt encoding, all controlled through your text prompt. Feature keywords: comfyui-prompt-control, schedule, macros, attention couple, loractl, A1111"
version = "3.0.0-beta.7"
version = "3.0.0-beta.9"
license = { file = "LICENSE" }
requires-python = ">= 3.10"
+2 -7
View File
@@ -461,7 +461,7 @@ def test_textencode_lora_with_schedule():
def test_textencode_custom():
r = te("NODE(CLIPTextEncode)simple [test:0.1,0.5] prompt")
r = te("NODE(CLIPTextEncode)simple [test:0.1,0.5] $p SEG(p) prompt")
assert r == {
"result": (["UID.0.0.8", 0],),
"expand": {
@@ -538,12 +538,7 @@ def test_textencode_custom_extra():
},
"UID.0.0.5": {
"class_type": "CustomTextEncode",
"inputs": {
"clip": [0, 0],
"prompt": "simple prompt",
"image": ["1", 0],
"option": "test"
},
"inputs": {"clip": [0, 0], "prompt": "simple prompt", "image": ["1", 0], "option": "test"},
},
"UID.0.0.6": {
"class_type": "ConditioningSetTimestepRange",
+6
View File
@@ -342,6 +342,12 @@ def test_cornercase_corrected(parse):
assert p.parsed_prompt[1:] == p2.parsed_prompt
def test_ltgt_in_schedule(parse):
p = parse("This should [<parse> correctly:be <Picture 1>:0.1]<lora:test:1>")
assert_prompt(p, 0.1, 0.1, "This should <parse> correctly", ("test", 1.0, 1.0))
assert_prompt(p, 0.15, 1.0, "This should be <Picture 1>", ("test", 1.0, 1.0))
def test_floats(parse):
p = parse("[a:b:0.5] [c:d:e:0.2,0.7] <lora:test:-0.3>")
p2 = parse("[a:b:.5] [c:d:e:.2,.7] <lora:test:-.3>")