Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d113d4ba78 | ||
|
|
5bd1d04dcd | ||
|
|
7c10770e07 | ||
|
|
69ea298174 | ||
|
|
4ba4b28bb2 | ||
|
|
f728866b90 | ||
|
|
306c02f57b | ||
|
|
2c519310ac | ||
|
|
3f23d1b14a | ||
|
|
148776fe5d | ||
|
|
cef4a80440 | ||
|
|
cd642b5d42 | ||
|
|
2b323da9a9 | ||
|
|
79b3675c4f | ||
|
|
b8d5b7a7c4 | ||
|
|
a5da586dc5 | ||
|
|
01aa061bef | ||
|
|
2fab4be810 | ||
|
|
127acb7018 |
@@ -8,14 +8,19 @@ on:
|
||||
- "pyproject.toml"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
uses: ./.github/workflows/tests.yml
|
||||
tests_with_comfy:
|
||||
uses: ./.github/workflows/tests_with_comfy.yml
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
needs: [tests, tests_with_comfy]
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
name: Execute tests
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
uses: ./.github/workflows/tests.yml
|
||||
@@ -1,9 +1,11 @@
|
||||
name: Run tests
|
||||
name: Run parser tests
|
||||
on:
|
||||
- workflow_call
|
||||
- workflow_dispatch
|
||||
- push
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
run-parser-tests:
|
||||
name: Run parser tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -13,4 +15,4 @@ jobs:
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install -r requirements.txt
|
||||
- run: python -m prompt_control.test
|
||||
- run: python -m prompt_control.test_parser
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Run tests requiring ComfyUI
|
||||
on:
|
||||
- workflow_call
|
||||
- workflow_dispatch
|
||||
- push:
|
||||
paths:
|
||||
- prompt_control/nodes_lazy.py
|
||||
- prompt_control/utils.py
|
||||
|
||||
|
||||
jobs:
|
||||
run-graph-tests:
|
||||
name: Run graph tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Check out ComfyUI
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: comfyanonymous/ComfyUI
|
||||
path: ComfyUI
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install -r requirements.txt
|
||||
- run: PYTHONPATH=ComfyUI python -m prompt_control.test_graph
|
||||
@@ -6,6 +6,9 @@ format:
|
||||
find . -name "*.py" | xargs black -l 120
|
||||
|
||||
test:
|
||||
python -m prompt_control.test
|
||||
python -m prompt_control.test_parser
|
||||
|
||||
test_graph:
|
||||
PYTHONPATH=../../ python -m prompt_control.test_graph
|
||||
|
||||
.PHONY: check format all
|
||||
|
||||
@@ -8,7 +8,7 @@ A `Basic Text to Image` template is included with the extension, and can be load
|
||||
|
||||
Prompt control has been almost completely rewritten. It now uses ComfyUI's lazy execution to build graphs from the text prompt at runtime. The generated graph is often exactly equivalent to a manually built workflow using native ComfyUI nodes. There are no more weird sampling hooks that could cause problems with other nodes
|
||||
|
||||
Prompt Control also comes with `PCTextEncode`, which provides advanced text encoding with many additional features compared to ComfyUI's base `CLIPTextEncode`.
|
||||
Prompt Control comes with `PCTextEncode`, which provides advanced text encoding with many additional features compared to ComfyUI's base `CLIPTextEncode`.
|
||||
|
||||
### Removed features
|
||||
|
||||
@@ -16,10 +16,6 @@ Prompt Control also comes with `PCTextEncode`, which provides advanced text enco
|
||||
- LoRA block weight integration; ditto, for now.
|
||||
|
||||
|
||||
### Is it stable now?
|
||||
|
||||
Unless I run into bugs or significant annoyances that require changing the interface, it probably won't change too much, but until I tag 2.0, everything can change.
|
||||
|
||||
### Everything broke, where are the old nodes?
|
||||
|
||||
If you really need them, you can install the [legacy nodes](https://github.com/asagi4/comfyui-prompt-control-legacy). However, I will not fix bugs in those nodes, and I strongly recommend just migrating your workflows to the new nodes.
|
||||
|
||||
+38
-1
@@ -196,7 +196,44 @@ The order of the `FEATHER` and `MASK` calls doesn't matter; you can have `FEATHE
|
||||
|
||||
# Experimental features
|
||||
|
||||
Experimental features are unstable and may disappear or break without warning.
|
||||
Experimental features are unstable and may disappear or change without warning.
|
||||
|
||||
## DEF
|
||||
|
||||
You can define "prompt macros" by using `DEF`:
|
||||
```
|
||||
DEF(MYMACRO=this is a prompt)
|
||||
[(MYMACRO:0.6):(MYMACRO:1.1):0.5]
|
||||
```
|
||||
is equivalent to
|
||||
```
|
||||
[(this is a prompt:0.5):(this is a prompt:1.1):0.5]
|
||||
```
|
||||
|
||||
It's also possible to give parameters to a macro:
|
||||
```
|
||||
DEF(MYMACRO=[(prompt $1:$2):(prompt $1:$3):$4])
|
||||
MYMACRO(test; 1.1; 0.7; 0.2)
|
||||
```
|
||||
gives
|
||||
```
|
||||
[(prompt test:1.1):(prompt test:0.7):0.2]
|
||||
```
|
||||
in this form, the variables $N (where N is any number corresponding to a positional parameter) will be replaced with the given parameter. The parameters must be separated with a semicolon.
|
||||
|
||||
Note that unspecified parameters will not be substituted:
|
||||
```
|
||||
DEF(mything=a $1 b $2)
|
||||
mything
|
||||
mything(A)
|
||||
```
|
||||
gives
|
||||
```
|
||||
a $1 b $2
|
||||
a A b $2
|
||||
```
|
||||
|
||||
Macros are expanded before any other parsing takes place. The expansion continues until no further changes occur. Recursion will raise an error.
|
||||
|
||||
## Attention masking
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
@@ -1,8 +1,9 @@
|
||||
import logging
|
||||
from .parser import parse_prompt_schedules
|
||||
from comfy_execution.graph_utils import GraphBuilder, is_link
|
||||
from comfy_execution.graph import ExecutionBlocker
|
||||
|
||||
from .prompts import get_function
|
||||
from .utils import get_function
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
@@ -88,8 +89,12 @@ def create_hook_nodes_for_lora(graph, path, info, existing_node, start_pct, end_
|
||||
def build_lora_schedule(graph, schedule, model, clip, apply_hooks=True, return_hooks=True):
|
||||
# This gets rid of non-existent LoRAs
|
||||
consolidated = consolidate_schedule(schedule)
|
||||
non_scheduled = find_nonscheduled_loras(consolidated)
|
||||
model, clip = create_lora_loader_nodes(graph, model, clip, non_scheduled)
|
||||
if model is not None:
|
||||
non_scheduled = find_nonscheduled_loras(consolidated)
|
||||
model, clip = create_lora_loader_nodes(graph, model, clip, non_scheduled)
|
||||
else:
|
||||
non_scheduled = {}
|
||||
model = ExecutionBlocker("No model provided to PCLazyLoRALoader or PCLazyLoRALoaderAdvanced")
|
||||
|
||||
hook_nodes = {}
|
||||
start_pct = 0.0
|
||||
@@ -170,7 +175,7 @@ class PCLazyLoraLoaderAdvanced:
|
||||
|
||||
def apply(self, model, clip, text, unique_id, apply_hooks=True, tags="", start=0.0, end=1.0):
|
||||
schedule = parse_prompt_schedules(text, filters=tags, start=start, end=end)
|
||||
graph = GraphBuilder(f"PCLazyLoraLoaderAdvanced-{unique_id}")
|
||||
graph = GraphBuilder(f"{unique_id}-")
|
||||
return build_lora_schedule(graph, schedule, model, clip, apply_hooks=apply_hooks, return_hooks=True)
|
||||
|
||||
|
||||
@@ -180,10 +185,10 @@ class PCLazyLoraLoader:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"optional": {
|
||||
"model": ("MODEL", {"rawLink": True}),
|
||||
"clip": ("CLIP", {"rawLink": True}),
|
||||
"text": ("STRING", {"multiline": True}),
|
||||
"text": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
@@ -196,9 +201,14 @@ class PCLazyLoraLoader:
|
||||
CATEGORY = "promptcontrol"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, model, clip, text, unique_id):
|
||||
graph = GraphBuilder(f"PCLazyLoraLoader-{unique_id}")
|
||||
def apply(self, unique_id, model=None, clip=None, text=""):
|
||||
graph = GraphBuilder(f"{unique_id}-")
|
||||
schedule = parse_prompt_schedules(text)
|
||||
if model is None and clip is None:
|
||||
return (
|
||||
ExecutionBlocker("No model input provided to PCLazyLoraLoader"),
|
||||
ExecutionBlocker("No clip input provided to PCLazyLoraLoader"),
|
||||
)
|
||||
return build_lora_schedule(graph, schedule, model, clip, apply_hooks=True, return_hooks=False)
|
||||
|
||||
|
||||
@@ -247,6 +257,7 @@ class PCLazyTextEncode:
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {"clip": ("CLIP", {"rawLink": True}), "text": ("STRING", {"multiline": True})},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("CONDITIONING",)
|
||||
@@ -254,9 +265,9 @@ class PCLazyTextEncode:
|
||||
CATEGORY = "promptcontrol"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, clip, text):
|
||||
def apply(self, clip, text, unique_id):
|
||||
schedules = parse_prompt_schedules(text)
|
||||
graph = GraphBuilder()
|
||||
graph = GraphBuilder(f"{unique_id}-")
|
||||
return build_scheduled_prompts(graph, schedules, clip)
|
||||
|
||||
|
||||
@@ -281,7 +292,7 @@ class PCLazyTextEncodeAdvanced:
|
||||
|
||||
def apply(self, clip, text, unique_id, tags="", start=0.1, end=1.0):
|
||||
schedules = parse_prompt_schedules(text, filters=tags, start=start, end=end)
|
||||
graph = GraphBuilder(f"PCLazyTextEncodeAdvanced-{unique_id}")
|
||||
graph = GraphBuilder(f"{unique_id}-")
|
||||
return build_scheduled_prompts(graph, schedules, clip)
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,71 @@
|
||||
import logging
|
||||
from .parser import parse_prompt_schedules
|
||||
from .nodes_lazy import NODE_CLASS_MAPPINGS as LAZY_NODES
|
||||
import json
|
||||
import folder_paths
|
||||
from pathlib import Path
|
||||
from comfy_execution.graph_utils import is_link
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
|
||||
class PCSaveExpandedWorkflow:
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"any": ("*", {}),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "DYNPROMPT",
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(self, input_types):
|
||||
return True
|
||||
|
||||
OUTPUT_NODE = True
|
||||
RETURN_TYPES = ()
|
||||
CATEGORY = "promptcontrol/tools"
|
||||
DESCRIPTION = "Saves the current expanded dynamic prompt into a JSON file"
|
||||
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, any, prompt):
|
||||
full_output_folder, filename, counter, subfolder, prefix = folder_paths.get_save_image_path(
|
||||
"pc_workflow_debug", self.output_dir
|
||||
)
|
||||
p = {}
|
||||
input_replace_map = {}
|
||||
for node in prompt.all_node_ids():
|
||||
n = prompt.get_node(node)
|
||||
t = n["class_type"]
|
||||
if t in LAZY_NODES:
|
||||
expanded_prompt = LAZY_NODES[t]().apply(**n["inputs"], unique_id=node)
|
||||
for k in expanded_prompt["expand"]:
|
||||
p[k] = expanded_prompt["expand"][k]
|
||||
for i, _ in enumerate(expanded_prompt["result"]):
|
||||
input_replace_map[(node, i)] = [k, i]
|
||||
else:
|
||||
p[node] = n
|
||||
for k in p:
|
||||
for ik in p[k]["inputs"]:
|
||||
x = p[k]["inputs"][ik]
|
||||
if is_link(x) and tuple(x) in input_replace_map:
|
||||
p[k]["inputs"][ik] = input_replace_map[tuple(x)]
|
||||
file = f"{filename}_{counter:05}_.json"
|
||||
full_path = Path(full_output_folder) / file
|
||||
with open(full_path, "w") as f:
|
||||
log.info(f"Saving workflow to {full_path}")
|
||||
json.dump(p, f)
|
||||
|
||||
return ()
|
||||
|
||||
|
||||
class PCSetLogLevel:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -156,6 +218,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"PCAddMaskToCLIPMany": PCAddMaskToCLIPMany,
|
||||
"PCSetLogLevel": PCSetLogLevel,
|
||||
"PCExtractScheduledPrompt": PCExtractScheduledPrompt,
|
||||
"PCSaveExpandedWorkflow": PCSaveExpandedWorkflow,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -164,4 +227,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"PCAddMaskToCLIPMany": "PC: Attach Mask (multi)",
|
||||
"PCSetLogLevel": "PC: Configure Logging (for debug)",
|
||||
"PCExtractScheduledPrompt": "PC: Extract Scheduled Prompt",
|
||||
"PCSaveExpandedWorkflow": "PC: Save Expanded Workflow (for debug)",
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# vim: sw=4 ts=4
|
||||
import lark
|
||||
import logging
|
||||
from math import ceil
|
||||
@@ -344,7 +345,7 @@ class PromptSchedule(object):
|
||||
return len(self.parsed_prompt) - 1, self.parsed_prompt[-1]
|
||||
|
||||
|
||||
def replace_defs(text):
|
||||
def replace_def(text):
|
||||
text, defs = get_function(text, "DEF", defaults=None)
|
||||
res = text
|
||||
prevres = text
|
||||
@@ -359,19 +360,37 @@ def replace_defs(text):
|
||||
while True:
|
||||
iterations += 1
|
||||
if iterations > 10:
|
||||
log.error("Unable to resolve DEFs, make sure there are no cycles!")
|
||||
raise ValueError("Unable to resolve DEFs, make sure there are no cycles!")
|
||||
return text
|
||||
for search, replace in replacements:
|
||||
res = re.sub(rf"\b{re.escape(search)}\b", replace, res)
|
||||
res = substitute_defcall(res, search, replace)
|
||||
res = substitute_def(res, search, replace)
|
||||
if res == prevres:
|
||||
break
|
||||
prevres = res
|
||||
if res != text:
|
||||
if res.strip() != text.strip():
|
||||
res = res.strip()
|
||||
log.info("DEFs expanded to: %s", res)
|
||||
return res
|
||||
|
||||
|
||||
def substitute_def(text, search, replace):
|
||||
return re.sub(rf"\b{re.escape(search)}\b", replace, text)
|
||||
|
||||
|
||||
def substitute_defcall(text, search, replace):
|
||||
text, defns = get_function(text, search, defaults=None, placeholder=f"DEFNCALL{search}")
|
||||
for i, defn in enumerate(defns):
|
||||
ph = f"\0DEFNCALL{search}{i}\0"
|
||||
paramvals = [x.strip() for x in defn.split(";")]
|
||||
r = replace
|
||||
for i, v in enumerate(paramvals):
|
||||
r = re.sub(rf"\${i+1}\b", v, r)
|
||||
text = text.replace(ph, r)
|
||||
return text
|
||||
|
||||
|
||||
@lru_cache
|
||||
def parse_prompt_schedules(prompt, **kwargs):
|
||||
prompt = replace_defs(prompt)
|
||||
prompt = replace_def(prompt)
|
||||
return PromptSchedule(prompt, **kwargs)
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import unittest
|
||||
import unittest.mock as mock
|
||||
|
||||
|
||||
def find_file(name):
|
||||
names = {"test": "test.safetensors", "other": "some/other.safetensors"}
|
||||
return names.get(name)
|
||||
|
||||
|
||||
class GraphTests(unittest.TestCase):
|
||||
maxDiff = 4096
|
||||
|
||||
@mock.patch("prompt_control.utils.lora_name_to_file", find_file)
|
||||
def test_textencode(self):
|
||||
clip = [0, 0]
|
||||
from .nodes_lazy import PCLazyTextEncode, PCLazyTextEncodeAdvanced
|
||||
|
||||
for p in ["test", "[test:0.2] test", "[test[test::0.5]]<lora:test:1>"]:
|
||||
r1 = PCLazyTextEncode().apply(clip, p, "UID")
|
||||
r2 = PCLazyTextEncodeAdvanced().apply(clip, p, "UID")
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
r = PCLazyTextEncode().apply(clip, "test<lora:test:1>", "UID")
|
||||
self.assertEqual(
|
||||
r,
|
||||
{
|
||||
"result": (["UID-2", 0],),
|
||||
"expand": {
|
||||
"UID-1": {"class_type": "PCTextEncode", "inputs": {"clip": [0, 0], "text": "test"}},
|
||||
"UID-2": {
|
||||
"class_type": "ConditioningSetTimestepRange",
|
||||
"inputs": {"conditioning": ["UID-1", 0], "start": 0.0, "end": 1.0},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
r = PCLazyTextEncode().apply(clip, "simple [test:0.1,0.5] prompt<lora:test:1>", "UID")
|
||||
self.assertEqual(
|
||||
r,
|
||||
{
|
||||
"result": (["UID-8", 0],),
|
||||
"expand": {
|
||||
"UID-1": {"class_type": "PCTextEncode", "inputs": {"clip": [0, 0], "text": "simple prompt"}},
|
||||
"UID-2": {
|
||||
"class_type": "ConditioningSetTimestepRange",
|
||||
"inputs": {"conditioning": ["UID-1", 0], "start": 0.0, "end": 0.1},
|
||||
},
|
||||
"UID-3": {"class_type": "PCTextEncode", "inputs": {"clip": [0, 0], "text": "simple test prompt"}},
|
||||
"UID-4": {
|
||||
"class_type": "ConditioningSetTimestepRange",
|
||||
"inputs": {"conditioning": ["UID-3", 0], "start": 0.1, "end": 0.5},
|
||||
},
|
||||
"UID-5": {"class_type": "PCTextEncode", "inputs": {"clip": [0, 0], "text": "simple prompt"}},
|
||||
"UID-6": {
|
||||
"class_type": "ConditioningSetTimestepRange",
|
||||
"inputs": {"conditioning": ["UID-5", 0], "start": 0.5, "end": 1.0},
|
||||
},
|
||||
"UID-7": {
|
||||
"class_type": "ConditioningCombine",
|
||||
"inputs": {"conditioning_1": ["UID-2", 0], "conditioning_2": ["UID-4", 0]},
|
||||
},
|
||||
"UID-8": {
|
||||
"class_type": "ConditioningCombine",
|
||||
"inputs": {"conditioning_1": ["UID-7", 0], "conditioning_2": ["UID-6", 0]},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@mock.patch("prompt_control.utils.lora_name_to_file", find_file)
|
||||
def test_loraloader(self):
|
||||
from .nodes_lazy import PCLazyLoraLoader, PCLazyLoraLoaderAdvanced
|
||||
|
||||
model = [0, 1]
|
||||
clip = [0, 0]
|
||||
with self.assertLogs("comfyui-prompt-control", level="WARNING") as cm:
|
||||
result = PCLazyLoraLoader().apply(model, clip, "prompt here <lora:nonexistent:1.0:0.5>", "UID")["expand"]
|
||||
result_adv = PCLazyLoraLoaderAdvanced().apply(model, clip, "prompt here <lora:nonexistent:1.0:0.5>", "UID")[
|
||||
"expand"
|
||||
]
|
||||
self.assertIn("LoRA 'nonexistent' not found", cm.output[0])
|
||||
self.assertIn("LoRA 'nonexistent' not found", cm.output[1])
|
||||
self.assertEqual(result, {})
|
||||
self.assertEqual(result_adv, {})
|
||||
|
||||
result = PCLazyLoraLoader().apply(model, clip, "<lora:test:1>", "UID")["expand"]
|
||||
result2 = PCLazyLoraLoader().apply(model, clip, "prompt here <lora:test:1.0:0.5><lora:test:0:0.5>", "UID")[
|
||||
"expand"
|
||||
]
|
||||
result3 = PCLazyLoraLoaderAdvanced().apply(
|
||||
model, clip, "prompt here <lora:test:1.0:0.5><lora:test:0:0.5>", "UID"
|
||||
)["expand"]
|
||||
self.assertEqual(result, result2)
|
||||
self.assertEqual(result2, result3)
|
||||
self.assertEqual(
|
||||
result,
|
||||
{
|
||||
"UID-1": {
|
||||
"class_type": "LoraLoader",
|
||||
"inputs": {
|
||||
"model": [0, 1],
|
||||
"clip": [0, 0],
|
||||
"strength_model": 1.0,
|
||||
"strength_clip": 1.0,
|
||||
"lora_name": "test.safetensors",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
result = PCLazyLoraLoader().apply(model, clip, "<lora:test:1><lora:other:0.5>", "UID")["expand"]
|
||||
self.assertEqual(
|
||||
result,
|
||||
{
|
||||
"UID-1": {
|
||||
"class_type": "LoraLoader",
|
||||
"inputs": {
|
||||
"model": [0, 1],
|
||||
"clip": [0, 0],
|
||||
"strength_model": 1.0,
|
||||
"strength_clip": 1.0,
|
||||
"lora_name": "test.safetensors",
|
||||
},
|
||||
},
|
||||
"UID-2": {
|
||||
"class_type": "LoraLoader",
|
||||
"inputs": {
|
||||
"model": ["UID-1", 0],
|
||||
"clip": ["UID-1", 1],
|
||||
"strength_model": 0.5,
|
||||
"strength_clip": 0.5,
|
||||
"lora_name": "some/other.safetensors",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
result = PCLazyLoraLoader().apply(model, clip, "prompt here <lora:test:1.0:0.5>", "UID")["expand"]
|
||||
self.assertEqual(
|
||||
result,
|
||||
{
|
||||
"UID-1": {
|
||||
"class_type": "LoraLoader",
|
||||
"inputs": {
|
||||
"model": [0, 1],
|
||||
"clip": [0, 0],
|
||||
"strength_model": 1.0,
|
||||
"strength_clip": 0.5,
|
||||
"lora_name": "test.safetensors",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
result = PCLazyLoraLoader().apply(model, clip, "prompt [<lora:test:0.5>:0.5]", "UID")["expand"]
|
||||
result2 = PCLazyLoraLoaderAdvanced().apply(model, clip, "prompt [<lora:test:0.5>:0.5]", "UID")["expand"]
|
||||
self.assertEqual(result, result2)
|
||||
expected = {
|
||||
"UID-1": {
|
||||
"class_type": "CreateHookLora",
|
||||
"inputs": {"lora_name": "test.safetensors", "strength_model": 0.5, "strength_clip": 0.5},
|
||||
},
|
||||
"UID-2": {
|
||||
"class_type": "CreateHookKeyframe",
|
||||
"inputs": {"strength_mult": 0.0, "start_percent": 0.0},
|
||||
},
|
||||
"UID-3": {
|
||||
"class_type": "CreateHookKeyframe",
|
||||
"inputs": {
|
||||
"start_percent": 0.5,
|
||||
"prev_hook_kf": ["UID-2", 0],
|
||||
"strength_mult": 1.0,
|
||||
},
|
||||
},
|
||||
"UID-4": {
|
||||
"class_type": "SetHookKeyframes",
|
||||
"inputs": {"hooks": ["UID-1", 0], "hook_kf": ["UID-3", 0]},
|
||||
},
|
||||
"UID-5": {
|
||||
"class_type": "SetClipHooks",
|
||||
"inputs": {
|
||||
"clip": [0, 0],
|
||||
"hooks": ["UID-4", 0],
|
||||
"apply_to_conds": True,
|
||||
"schedule_clip": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
self.assertEqual(result, expected)
|
||||
result2 = PCLazyLoraLoaderAdvanced().apply(model, clip, "prompt [<lora:test:0.5>:0.5]", "UID", start=0.6)[
|
||||
"expand"
|
||||
]
|
||||
self.assertEqual(
|
||||
result2,
|
||||
{
|
||||
"UID-1": {
|
||||
"class_type": "LoraLoader",
|
||||
"inputs": {
|
||||
"model": [0, 1],
|
||||
"clip": [0, 0],
|
||||
"strength_model": 0.5,
|
||||
"strength_clip": 0.5,
|
||||
"lora_name": "test.safetensors",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
result2 = PCLazyLoraLoaderAdvanced().apply(model, clip, "prompt [<lora:test:0.5>:0.5]", "UID", end=0.5)[
|
||||
"expand"
|
||||
]
|
||||
self.assertEqual(result2, {})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -99,6 +99,18 @@ class TestParser(unittest.TestCase):
|
||||
for k, v in prompts.items():
|
||||
self.assertPrompt(p, k, v[0], v[1])
|
||||
|
||||
p = parse("DEF(X=[($1):($1:$2):$2])X(test;0.7)")
|
||||
p2 = parse("[(test):(test:0.7):0.7]")
|
||||
self.assertEqual(p.parsed_prompt, p2.parsed_prompt)
|
||||
|
||||
p = parse("DEF(X=[($1):($1:$2):$2])DEF(Y=X(test;$1))Y(0.7) Y(0.5)")
|
||||
p2 = parse("[(test):(test:0.7):0.7] [(test):(test:0.5):0.5]")
|
||||
self.assertEqual(p.parsed_prompt, p2.parsed_prompt)
|
||||
|
||||
with self.assertRaises(ValueError) as c:
|
||||
parse("DEF(X=recurse Y) DEF(Y=recurse X) X")
|
||||
self.assertTrue("Unable to resolve DEFs" in str(c.exception))
|
||||
|
||||
def test_misc(self):
|
||||
p = parse("[[a:c:0.5]:0.7]")
|
||||
p2 = parse("[:[a:c:0.5]:0.7]")
|
||||
@@ -87,10 +87,11 @@ def find_closing_paren(text, start):
|
||||
return len(text)
|
||||
|
||||
|
||||
def get_function(text, func, defaults, return_func_name=False):
|
||||
def get_function(text, func, defaults, return_func_name=False, placeholder=""):
|
||||
rex = re.compile(rf"\b{func}\(", re.MULTILINE)
|
||||
instances = []
|
||||
match = rex.search(text)
|
||||
count = 0
|
||||
while match:
|
||||
# Match start, content start
|
||||
start, after_first_paren = match.span()
|
||||
@@ -102,8 +103,12 @@ def get_function(text, func, defaults, return_func_name=False):
|
||||
else:
|
||||
instances.append(args)
|
||||
|
||||
text = text[:start] + text[end + 1 :]
|
||||
if placeholder:
|
||||
text = text[:start] + f"\0{placeholder}{count}\0" + text[end + 1 :]
|
||||
else:
|
||||
text = text[:start] + text[end + 1 :]
|
||||
match = rex.search(text)
|
||||
count += 1
|
||||
return text, instances
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-prompt-control"
|
||||
description = "Nodes for convenient prompt editing, making many common operations prompt-controllable"
|
||||
version = "2.0.0-beta.8"
|
||||
version = "2.0.0-beta.9"
|
||||
license = { file = "LICENSE" }
|
||||
# some lark versions older than 1.1.9 apparently have a bug that breaks things, see https://github.com/asagi4/comfyui-prompt-control/issues/35
|
||||
dependencies = ["lark >= 1.1.9"]
|
||||
|
||||
Reference in New Issue
Block a user