Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9c5da7210 | ||
|
|
244ef49230 | ||
|
|
3a563e3ceb | ||
|
|
812ad90d17 | ||
|
|
7d9e8aa6ac | ||
|
|
f47428ea8c | ||
|
|
0aeeb50331 | ||
|
|
9931c6fa75 | ||
|
|
655f6ac4a1 | ||
|
|
136932de40 | ||
|
|
06a3f43a93 | ||
|
|
203a7ad45c | ||
|
|
7a566e6e9e | ||
|
|
fa288c226c | ||
|
|
e648f3bdc3 | ||
|
|
1bedc6ad53 | ||
|
|
de79f3c6af |
@@ -19,5 +19,5 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install pytest typing-extensions -r requirements.txt
|
||||
- run: pip install pytest typing-extensions
|
||||
- run: PYTHONPATH=ComfyUI pytest tests/test_parser.py
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: install-torch
|
||||
run: pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||
- name: install ComfyUI
|
||||
run: pip install pytest typing-extensions -r requirements.txt -r ComfyUI/requirements.txt
|
||||
run: pip install pytest typing-extensions -r ComfyUI/requirements.txt
|
||||
- name: Download clip_l.safetensors
|
||||
run: curl -LO https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors
|
||||
- name: Force Comfy to use the CPU
|
||||
|
||||
@@ -12,7 +12,7 @@ format:
|
||||
ruff format
|
||||
|
||||
test:
|
||||
PYTHONPATH=../../ pytest tests/test_parser.py tests/test_cutout.py $(ARGS)
|
||||
PYTHONPATH=../../ pytest tests/test_parser.py tests/test_cutout.py tests/test_macros.py $(ARGS)
|
||||
|
||||
test_graph:
|
||||
PYTHONPATH=../../ pytest tests/test_graph.py $(ARGS)
|
||||
@@ -20,6 +20,9 @@ test_graph:
|
||||
test_encode:
|
||||
PYTHONPATH=../../ pytest tests/test_encode.py $(ARGS)
|
||||
|
||||
test_workflow:
|
||||
PYTHONPATH=../../ pytest tests/test_workflow.py $(ARGS)
|
||||
|
||||
test_encode_both:
|
||||
TEST_TE="clip_l t5" PYTHONPATH=../../ pytest tests/test_encode.py $(ARGS)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ComfyUI prompt control
|
||||
|
||||
Control LoRA and prompt scheduling, advanced text encoding, regional prompting, and much more, through your text prompt. Generates dynamic graphs that are literally identical to handcrafted noodle soup.
|
||||
Control LoRA and prompt scheduling, advanced text encoding, regional prompting, and much more, through your text prompt. Prompt Control generates dynamic graphs that are literally identical to handcrafted noodle soup, condensing complicated workflows with dozens of nodes into simple text prompts.
|
||||
|
||||
Prompt Control comes with `PCTextEncode`, which provides advanced text encoding with many additional features compared to ComfyUI's base `CLIPTextEncode`.
|
||||
|
||||
@@ -14,17 +14,15 @@ A `Basic Text to Image` template is included with the extension, and can be load
|
||||
|
||||
## What can it do?
|
||||
|
||||
You can use text prompts to control the following:
|
||||
|
||||
- A1111-style prompt scheduling and filtering without noodle soup.
|
||||
- LoRA loading and [scheduling](/doc/schedules.md) via the prompt, using ComfyUI's hook system
|
||||
- LoRA loading and [scheduling](/doc/schedules.md) using ComfyUI's built-in hook system.
|
||||
- Masking, composition and area control ([regional prompting](/doc/regional_prompts.md)) with an implementation of [Attention Couple](/doc/attention_couple.md), also fully schedulable.
|
||||
- [Advanced prompt encoding](/doc/basic.md)
|
||||
- Per-encoder prompts for models with multiple text encoders, such as SDXL and Flux
|
||||
- Per-encoder prompts for models with multiple text encoders, such as SDXL and Flux.
|
||||
- Prompt combinators like `BREAK`, as well as `CAT`, `AVG()` and `AND` corresponding to ComfyUI's `ConditioningConcat`, `ConditioningAverage` and `ConditioningCombine` nodes.
|
||||
- Different weight interpretation types (ComfyUI, A1111, compel, etc.)
|
||||
- Prompt masking with an implementation of [cutoff](https://github.com/BlenderNeko/ComfyUI_Cutoff)
|
||||
- Simple [prompt macros](/doc/macros.md) with `DEF`
|
||||
- Prompt masking with an implementation of [cutoff](https://github.com/BlenderNeko/ComfyUI_Cutoff).
|
||||
- Organize complicated prompts with [segments and prompt macros](/doc/macros.md).
|
||||
|
||||
All features are fully schedulable unless otherwise stated. See the [scheduling syntax documentation](doc/schedules.md) to get started.
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ Prompt operators are processed in the following order, meaning that all features
|
||||
|
||||
- DEF macros are expanded
|
||||
- Scheduling is expanded, and for each scheduled prompt:
|
||||
- SEGs are processed and the template is expanded
|
||||
- The prompt is split by AND, and for each:
|
||||
- Prompts are split by COUPLE. and for each:
|
||||
- Most functions (like MASK) and cutoffs are evaluated
|
||||
|
||||
@@ -58,3 +58,40 @@ a "$1" b "$2"
|
||||
a "" b "$2"
|
||||
a "A" b "$2"
|
||||
```
|
||||
|
||||
## SEG: Split your prompt into named segments
|
||||
|
||||
Syntax: `SEG(segment_name)`
|
||||
|
||||
To help with organizing prompts, you can use the `SEG` function. For example:
|
||||
```
|
||||
This is a comic
|
||||
Top panel: $CAT. $SEG3
|
||||
Bottom panel: $DOG
|
||||
|
||||
SEG(DOG)
|
||||
A dog chasing its
|
||||
tail in a living room.
|
||||
SEG(CAT)
|
||||
|
||||
a sleeping cat
|
||||
|
||||
SEG
|
||||
The cat has orange fur with white stripes
|
||||
```
|
||||
This produces:
|
||||
```
|
||||
This is a comic
|
||||
Top panel: a sleeping cat. The cat has orange fur with white stripes
|
||||
Bottom panel: A dog chasing its
|
||||
tail in a living room.
|
||||
```
|
||||
Unlike macros, SEG is processed *after* scheduling syntax.
|
||||
|
||||
In this case, the first section before any `SEG` becomes the *template* and any text after a `SEG` call becomes part of that segment. Whitespace is stripped from the start and end of segments and the template.
|
||||
|
||||
In the template, you can refer to segments by either their index (starting from 1) or the given name, prefixed with a `$SEG`, so in this example, `$SEG1` is the same as `$PANEL1`
|
||||
|
||||
Segments can also refer to each other. Recursion will terminate, but produces weird outputs.
|
||||
|
||||
Naming segments is optional, in which case you will have to refer to it by its index.
|
||||
|
||||
@@ -71,7 +71,8 @@ class AttentionCoupleHook(TransformerOptionsHook):
|
||||
}
|
||||
self.has_negpip = False
|
||||
# calculate later. All clones must refer to the same kv dict
|
||||
self.kv = {"k": [], "v": []}
|
||||
# The type is here to shut up the type checker
|
||||
self.kv: dict[str, list] = {"k": None, "v": None}
|
||||
|
||||
def initialize_regions(self, base_cond, conds, fill):
|
||||
self.num_conds = len(conds) + 1
|
||||
|
||||
@@ -4,12 +4,35 @@ from __future__ import annotations
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .utils import find_closing_paren, get_function
|
||||
from .utils import find_closing_paren, get_function, split_by_function
|
||||
|
||||
logging.basicConfig()
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
|
||||
def substitute_template(template, segments):
|
||||
def _substitute(template, segments, stack):
|
||||
for name, value in sorted(segments):
|
||||
value = substitute_var(value, name, "")
|
||||
if name not in stack:
|
||||
stack.add(name)
|
||||
value = _substitute(value, segments, stack)
|
||||
stack.remove(name)
|
||||
template = substitute_var(template, name, value)
|
||||
return template
|
||||
|
||||
return _substitute(template, segments, set())
|
||||
|
||||
|
||||
def expand_segs(text):
|
||||
template, segments = split_by_function(text, "SEG", defaults=[""], require_args=True)
|
||||
named_segs = [(f.args[0].strip() or f"SEG{i + 1}", c.strip()) for i, (c, f) in enumerate(segments)]
|
||||
|
||||
new_text = substitute_template(template, named_segs).strip()
|
||||
if new_text != text.strip():
|
||||
log.debug("Template expanded to: %s", new_text)
|
||||
return new_text
|
||||
|
||||
|
||||
def parse_search(search):
|
||||
arg_start = search.find("(")
|
||||
args = ""
|
||||
@@ -60,6 +83,11 @@ def expand_macros(text):
|
||||
return res
|
||||
|
||||
|
||||
def substitute_var(text, name, replace):
|
||||
name = re.escape(str(name))
|
||||
return re.sub(rf"\${name}\b", replace, text)
|
||||
|
||||
|
||||
def substitute_defcall(text, search, replace):
|
||||
name, default_args = search
|
||||
text, defns = get_function(text, name, defaults=None, placeholder=f"DEFNCALL{name}", require_args=False)
|
||||
@@ -72,10 +100,10 @@ def substitute_defcall(text, search, replace):
|
||||
paramvals = [x.strip() for x in parameters[0].split(";")]
|
||||
r = replace
|
||||
for i, v in enumerate(paramvals):
|
||||
r = re.sub(rf"\${i + 1}\b", v, r)
|
||||
r = substitute_var(r, i + 1, v)
|
||||
|
||||
for i, v in enumerate(default_args):
|
||||
r = re.sub(rf"\${i + 1}\b", v, r)
|
||||
r = substitute_var(r, i + 1, v)
|
||||
|
||||
text = text.replace(ph, r)
|
||||
return text
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
from comfy_api.latest import io
|
||||
|
||||
from .macros import expand_segs
|
||||
from .prompts import encode_prompt
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
@@ -29,6 +30,7 @@ class PCTextEncodeWithRange(io.ComfyNode):
|
||||
log.debug("PCTextEncode: Encoding '%s'", text)
|
||||
defaults = clip.patcher.model_options.get("x-promptcontrol.defaults", {})
|
||||
masks = clip.patcher.model_options.get("x-promptcontrol.masks", None)
|
||||
text = expand_segs(text)
|
||||
out = encode_prompt(clip, text, start, end, defaults, masks)
|
||||
return io.NodeOutput(out)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
if os.environ.get("PC_USE_OLD_PARSER", "0") != "0":
|
||||
if os.environ.get("PC_USE_OLD_PARSER", "0") != "1":
|
||||
log.info("Using new parser implementation. Set PC_USE_OLD_PARSER=1 to use old parser instead")
|
||||
from .parser_parsy import parse_prompt_schedules # noqa
|
||||
else:
|
||||
|
||||
@@ -10,7 +10,6 @@ import lark
|
||||
from .macros import expand_macros
|
||||
from .utils import flatten
|
||||
|
||||
logging.basicConfig()
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
if lark.__version__ == "0.12.0":
|
||||
|
||||
@@ -348,9 +348,14 @@ non_special = regex(r"[^:\[\]()|\\<>#]+").map(Text)
|
||||
filename = regex(r"[^:<>]+")
|
||||
|
||||
comment = string("#") >> any_char.until(eof | char_from("\n")) >> success(empty)
|
||||
escape = (string("\\") >> char_from("\\[]:#")).map(Text)
|
||||
escape = (string("\\") >> char_from("\\[]:#") | string(r"\(") | string(r"\)")).map(Text)
|
||||
emphasis = seq(lpar, (prompt | col).at_least(0), rpar)
|
||||
number = (digit.at_least(1) + string(".") * 1 + digit.many() | digit.at_least(1)).concat().map(float)
|
||||
sign = string("+") | string("-")
|
||||
number = (
|
||||
(sign.optional("") + (digit.many() + string(".") * 1 + digit.many() | digit.at_least(1)).concat())
|
||||
.concat()
|
||||
.map(float)
|
||||
)
|
||||
|
||||
opt_prompt = prompt.optional(empty)
|
||||
step_range = seq(number | tag, (comma >> number).optional())
|
||||
|
||||
@@ -241,7 +241,7 @@ def encode_prompt_segment(
|
||||
can_break = {}
|
||||
for k in empty:
|
||||
tokenizer = getattr(clip.tokenizer, f"clip_{k}", getattr(clip.tokenizer, k, None))
|
||||
can_break[k] = tokenizer and tokenizer.pad_to_max_length
|
||||
can_break[k] = tokenizer and getattr(tokenizer, "pad_to_max_length", False)
|
||||
|
||||
clip = hook_te(clip, empty.keys(), style, normalization, extra)
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-prompt-control"
|
||||
description = "Provides nodes for prompt editing and LoRA scheduling, advanced regional prompting (including attention masking) and more, all controlled through your text prompt"
|
||||
version = "3.0.0-beta.2"
|
||||
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.4"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
requires-python = ">= 3.10"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Nothing for now
|
||||
@@ -0,0 +1,68 @@
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
|
||||
from prompt_control.macros import expand_macros, expand_segs
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, result",
|
||||
[
|
||||
("DEF(X(a;b)=$1 $2 $3 d)X(A) X(A;B;C)", "A b $3 d A B C d"),
|
||||
(
|
||||
"DEF(MACRO()=[empty:$1:$2])MACRO MACRO(;) MACRO(;0.5) MACRO(a;0.5)",
|
||||
"[empty::$2] [empty::] [empty::0.5] [empty:a:0.5]",
|
||||
),
|
||||
("DEF(X=$1)DEF(Y()=$1)[X Y][X() Y()][X(1) Y(1)]", "[$1 ][ ][1 1]"),
|
||||
],
|
||||
)
|
||||
def test_basic_macro(text, result):
|
||||
assert expand_macros(text) == result
|
||||
|
||||
|
||||
def test_macro_recursion():
|
||||
with pytest.raises(ValueError) as c:
|
||||
expand_macros("DEF(X=recurse Y) DEF(Y=recurse X) X")
|
||||
assert "Unable to resolve DEFs" in str(c.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input, output",
|
||||
[
|
||||
(
|
||||
"""\
|
||||
A red $b and
|
||||
a blue $a
|
||||
SEG(a)
|
||||
cat
|
||||
SEG(b)
|
||||
|
||||
dog
|
||||
SEG(c)""",
|
||||
"A red dog and\na blue cat",
|
||||
),
|
||||
(
|
||||
"""\
|
||||
$a and $b
|
||||
SEG(a)
|
||||
cat, $b
|
||||
SEG(b)
|
||||
dog, $c
|
||||
SEG(c)
|
||||
tiger
|
||||
""",
|
||||
"cat, dog, tiger and dog, tiger",
|
||||
),
|
||||
(
|
||||
"""\
|
||||
$a
|
||||
SEG(a)
|
||||
a $b
|
||||
SEG(b)
|
||||
b $a""",
|
||||
"a b a b $a",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_segments(input, output):
|
||||
assert expand_segs(dedent(input)) == output
|
||||
+9
-21
@@ -2,8 +2,6 @@ import os
|
||||
|
||||
import pytest
|
||||
|
||||
from prompt_control.macros import expand_macros
|
||||
|
||||
|
||||
def lora_dict(*loras):
|
||||
return {lora: {"weight": unet, "weight_clip": te} for lora, unet, te in loras}
|
||||
@@ -113,9 +111,9 @@ def test_basic_ok(parse):
|
||||
|
||||
@pytest.mark.parametrize("step", [0, 0.5, 1])
|
||||
def test_lora(step, parse):
|
||||
p = parse("This is a (lora:0.6) (prompt) with [no scheduling] features <lora:foo:0.5> <lora:bar:0.5:1.0>")
|
||||
p = parse("This is a (lora:0.6) (prompt) with [no scheduling] features <lora:foo:0.5> <lora:bar:0.5:-1.0>")
|
||||
expected = prompt(
|
||||
1.0, "This is a (lora:0.6) (prompt) with [no scheduling] features ", ("foo", 0.5, 0.5), ("bar", 0.5, 1.0)
|
||||
1.0, "This is a (lora:0.6) (prompt) with [no scheduling] features ", ("foo", 0.5, 0.5), ("bar", 0.5, -1.0)
|
||||
)
|
||||
assert prompts_match(p.at_step(step), expected)
|
||||
|
||||
@@ -223,23 +221,6 @@ def test_def(parse):
|
||||
p2 = parse("[(test):(test:0.7):0.7] [(test):(test:0.5):0.5]")
|
||||
assert p.parsed_prompt == p2.parsed_prompt
|
||||
|
||||
p = expand_macros("DEF(X(a;b)=$1 $2 $3 d)X(A) X(A;B;C)")
|
||||
assert p == "A b $3 d A B C d"
|
||||
|
||||
p = expand_macros("DEF(MACRO()=[empty:$1:$2])MACRO MACRO(;) MACRO(;0.5) MACRO(a;0.5)")
|
||||
assert p == "[empty::$2] [empty::] [empty::0.5] [empty:a:0.5]"
|
||||
|
||||
p = expand_macros("DEF(X=$1)DEF(Y()=$1)[X Y][X() Y()][X(1) Y(1)]")
|
||||
assert p == "[$1 ][ ][1 1]"
|
||||
|
||||
p = parse("DEF(test(1)=prompt $1)DEF(test2((a); (test))=[$1:$2:0.5])test test2")
|
||||
p2 = parse("prompt 1 [(a):(prompt 1):0.5]")
|
||||
assert p.parsed_prompt == p2.parsed_prompt
|
||||
|
||||
with pytest.raises(ValueError) as c:
|
||||
expand_macros("DEF(X=recurse Y) DEF(Y=recurse X) X")
|
||||
assert "Unable to resolve DEFs" in str(c.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, cases",
|
||||
@@ -251,6 +232,7 @@ def test_def(parse):
|
||||
),
|
||||
(r"[a\:b\\:c:0.5]", [(0.0, 0.5, "a:b\\"), (0.55, 1, r"c")]),
|
||||
(r"[a:\#b:0.5]", [(0.0, 0.5, "a"), (0.55, 1, "#b")]),
|
||||
(r"[a:b \(test\):0.2]", [(0, 0.2, r"a"), (0.25, 1, r"b \(test\)")]),
|
||||
],
|
||||
)
|
||||
def test_escapes(text, cases, parse):
|
||||
@@ -360,6 +342,12 @@ def test_cornercase_corrected(parse):
|
||||
assert p.parsed_prompt[1:] == p2.parsed_prompt
|
||||
|
||||
|
||||
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>")
|
||||
assert p.parsed_prompt == p2.parsed_prompt
|
||||
|
||||
|
||||
def test_alternating_lora(parse):
|
||||
p4 = parse("[cat|[dog:wolf<lora:canine:1>:0.5]:0.2]")
|
||||
for i, (text, *_loras) in enumerate(
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"1": {
|
||||
"inputs": {
|
||||
"text": "positive prompt",
|
||||
"clip": [
|
||||
"4",
|
||||
1
|
||||
]
|
||||
},
|
||||
"class_type": "PCLazyTextEncode",
|
||||
"_meta": {
|
||||
"title": "PC: Schedule prompt"
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"inputs": {
|
||||
"ckpt_name": "$TEST_CHECKPOINT"
|
||||
},
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"_meta": {
|
||||
"title": "Load Checkpoint"
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"inputs": {
|
||||
"seed": 0,
|
||||
"steps": 8,
|
||||
"cfg": 3,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"denoise": 1,
|
||||
"model": [
|
||||
"4",
|
||||
0
|
||||
],
|
||||
"positive": [
|
||||
"9",
|
||||
0
|
||||
],
|
||||
"negative": [
|
||||
"9",
|
||||
1
|
||||
],
|
||||
"latent_image": [
|
||||
"5",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "KSampler",
|
||||
"_meta": {
|
||||
"title": "KSampler"
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"inputs": {
|
||||
"text": "<lora:$TEST_LORA:1>",
|
||||
"model": [
|
||||
"2",
|
||||
0
|
||||
],
|
||||
"clip": [
|
||||
"2",
|
||||
1
|
||||
]
|
||||
},
|
||||
"class_type": "PCLazyLoraLoader",
|
||||
"_meta": {
|
||||
"title": "PC: Schedule LoRAs"
|
||||
}
|
||||
},
|
||||
"5": {
|
||||
"inputs": {
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"batch_size": 1
|
||||
},
|
||||
"class_type": "EmptyLatentImage",
|
||||
"_meta": {
|
||||
"title": "Empty Latent Image"
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"3",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"2",
|
||||
2
|
||||
]
|
||||
},
|
||||
"class_type": "VAEDecode",
|
||||
"_meta": {
|
||||
"title": "VAE Decode"
|
||||
}
|
||||
},
|
||||
"7": {
|
||||
"inputs": {
|
||||
"images": [
|
||||
"6",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "PreviewImage",
|
||||
"_meta": {
|
||||
"title": "Preview Image"
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"inputs": {
|
||||
"text": "worst quality,",
|
||||
"clip": [
|
||||
"4",
|
||||
1
|
||||
]
|
||||
},
|
||||
"class_type": "CLIPTextEncode",
|
||||
"_meta": {
|
||||
"title": "CLIP Text Encode (Prompt)"
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"positive": [
|
||||
"1",
|
||||
0
|
||||
],
|
||||
"negative": [
|
||||
"8",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "PCAttentionCoupleBatchNegative",
|
||||
"_meta": {
|
||||
"title": "PC: Attention Couple (batch negative)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from time import sleep
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def workflow(request):
|
||||
with open(str(request.path).replace(".py", ".json")) as f:
|
||||
data = f.read()
|
||||
data = data.replace("$TEST_CHECKPOINT", os.environ["PC_TEST_CHECKPOINT"])
|
||||
data = data.replace("$TEST_LORA", os.environ["PC_TEST_LORA"])
|
||||
return json.loads(data)
|
||||
|
||||
|
||||
def assert_prompt(url, p):
|
||||
timeout = 60
|
||||
r = requests.post(f"{url}/prompt", json={"prompt": p, "client_id": str(uuid.uuid4())}).json()
|
||||
prompt_id = r["prompt_id"]
|
||||
r = {"status": "pending"}
|
||||
while r["status"] in ["pending", "in_progress"]:
|
||||
sleep(1)
|
||||
assert timeout > 0
|
||||
timeout -= 1
|
||||
r = requests.get(f"{url}/api/jobs/{prompt_id}").json()
|
||||
assert r["status"] == "completed"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def comfyui():
|
||||
return os.environ.get("PC_TEST_COMFYUI", "http://localhost:8188")
|
||||
|
||||
|
||||
def test_workflow(workflow, comfyui):
|
||||
prompt = "DEF(blue=green)a blue dog and a cat sitting [COUPLE(0 0.5, 0 1) red (cat,:1.3) COUPLE(0.5 1, 0 1) (blue:1.2) dog,:0.1]"
|
||||
workflow["1"]["inputs"]["text"] = prompt
|
||||
assert_prompt(comfyui, workflow)
|
||||
Reference in New Issue
Block a user