Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a76cc8c72 | ||
|
|
6b1e2a5a8a | ||
|
|
9aee531c09 | ||
|
|
c08bf395a6 | ||
|
|
1964708997 | ||
|
|
1c4b5ce0c4 | ||
|
|
5eabbb419c | ||
|
|
53400a029b | ||
|
|
c39605eec4 | ||
|
|
dc62e638ed | ||
|
|
109cac16ef | ||
|
|
cf6c2b3e6a |
@@ -7,6 +7,9 @@ on:
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
uses: ./.github/workflows/tests.yml
|
||||
@@ -15,6 +18,7 @@ jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'asagi4' }}
|
||||
needs: [tests, tests_with_comfy]
|
||||
steps:
|
||||
- name: Check out code
|
||||
@@ -22,5 +26,4 @@ jobs:
|
||||
- name: Publish Custom Node
|
||||
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,11 +1,11 @@
|
||||
name: Run tests requiring ComfyUI
|
||||
on:
|
||||
- workflow_call
|
||||
- workflow_dispatch
|
||||
- push:
|
||||
paths:
|
||||
- prompt_control/nodes_lazy.py
|
||||
- prompt_control/utils.py
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- prompt_control/nodes_lazy.py
|
||||
- prompt_control/utils.py
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -26,6 +26,14 @@ switches from `a red cat` to `a blue cat` at 0.5 and to `a cat` at 0.7
|
||||
|
||||
**Note:** As a special case, `[cat:0.5]` is equivalent to `[:cat:0.5]` meaning it switches from empty to `cat` at 0.5.
|
||||
|
||||
### Using step numbers with the Advanced nodes
|
||||
|
||||
If you provide a non-zero value to `num_steps` to the `Advanced` versions of the lazy LoRA loaders, you will be able to use step numbers in prompts.
|
||||
|
||||
For now, a value between 0 and 1.0 will be interpreted as a percentage if it contains a ., and as an absolute step otherwise.
|
||||
|
||||
This is just syntactic sugar. Behind the scenes, the values are converted to percentages and have normal ComfyUI scheduling behaviour.
|
||||
|
||||
### Range expressions
|
||||
|
||||
You can also use `a [during:after:0.3,0.7]` as a shortcut. The prompt be `a` until 0.3, `a during` until 0.7, and then `a after`. This form is equivalent to `[[during:after:0.7]:0.3]`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from .parser import parse_prompt_schedules
|
||||
from comfy_execution.graph_utils import GraphBuilder, is_link
|
||||
|
||||
from comfy_execution.graph import ExecutionBlocker
|
||||
|
||||
from .utils import get_function
|
||||
@@ -86,7 +87,7 @@ def create_hook_nodes_for_lora(graph, path, info, existing_node, start_pct, end_
|
||||
return hook_node, next_keyframe
|
||||
|
||||
|
||||
def build_lora_schedule(graph, schedule, model, clip, apply_hooks=True, return_hooks=True):
|
||||
def build_lora_schedule(graph, schedule, model, clip, apply_hooks=True):
|
||||
# This gets rid of non-existent LoRAs
|
||||
consolidated = consolidate_schedule(schedule)
|
||||
if model is not None:
|
||||
@@ -129,7 +130,7 @@ def build_lora_schedule(graph, schedule, model, clip, apply_hooks=True, return_h
|
||||
n.set_input("hooks_B", h.out(0))
|
||||
res = n
|
||||
res = res.out(0)
|
||||
if apply_hooks:
|
||||
if clip is not None and apply_hooks:
|
||||
n = graph.node("SetClipHooks")
|
||||
n.set_input("clip", clip)
|
||||
n.set_input("hooks", res)
|
||||
@@ -137,13 +138,12 @@ def build_lora_schedule(graph, schedule, model, clip, apply_hooks=True, return_h
|
||||
n.set_input("schedule_clip", True)
|
||||
clip = n.out(0)
|
||||
|
||||
if clip is None:
|
||||
clip = ExecutionBlocker("No clip model provided to PCLazyLoRALoader or PCLazyLoRALoaderAdvanced")
|
||||
r = graph.finalize()
|
||||
log.debug("LazyLoraLoader built graph: %s", json.dumps(r))
|
||||
|
||||
if return_hooks:
|
||||
ret = (model, clip, res)
|
||||
else:
|
||||
ret = (model, clip)
|
||||
ret = (model, clip, res)
|
||||
|
||||
return {"result": ret, "expand": r}
|
||||
|
||||
@@ -154,16 +154,15 @@ class PCLazyLoraLoaderAdvanced:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {"multiline": True}),
|
||||
"optional": {
|
||||
"model": ("MODEL", {"rawLink": True}),
|
||||
"clip": ("CLIP", {"rawLink": True}),
|
||||
},
|
||||
"optional": {
|
||||
"text": ("STRING", {"multiline": True, "default": ""}),
|
||||
"apply_hooks": ("BOOLEAN", {"default": True}),
|
||||
"tags": ("STRING", {"default": ""}),
|
||||
"start": ("FLOAT", {"min": 0.0, "max": 1.0, "default": 0.0, "step": 0.01}),
|
||||
"end": ("FLOAT", {"min": 0.0, "max": 1.0, "default": 1.0, "step": 0.01}),
|
||||
"num_steps": ("INT", {"min": 0, "max": 10000, "default": 0, "step": 1}),
|
||||
},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
@@ -173,15 +172,16 @@ class PCLazyLoraLoaderAdvanced:
|
||||
CATEGORY = "promptcontrol"
|
||||
FUNCTION = "apply"
|
||||
|
||||
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)
|
||||
def apply(
|
||||
self, unique_id, model=None, clip=None, text="", apply_hooks=True, tags="", start=0.0, end=1.0, num_steps=0
|
||||
):
|
||||
schedule = parse_prompt_schedules(text, filters=tags, start=start, end=end, num_steps=num_steps)
|
||||
graph = GraphBuilder(f"{unique_id}-")
|
||||
return build_lora_schedule(graph, schedule, model, clip, apply_hooks=apply_hooks, return_hooks=True)
|
||||
r = build_lora_schedule(graph, schedule, model, clip, apply_hooks=apply_hooks)
|
||||
return r
|
||||
|
||||
|
||||
class PCLazyLoraLoader:
|
||||
CACHE_KEY = cache_key_lora
|
||||
|
||||
class PCLazyLoraLoader(PCLazyLoraLoaderAdvanced):
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
@@ -197,19 +197,12 @@ class PCLazyLoraLoader:
|
||||
"MODEL",
|
||||
"CLIP",
|
||||
)
|
||||
OUTPUT_TOOLTIPS = ("Returns a model and clip with LoRAs scheduled",)
|
||||
CATEGORY = "promptcontrol"
|
||||
FUNCTION = "apply"
|
||||
|
||||
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)
|
||||
def apply(self, *args, **kwargs):
|
||||
r = super().apply(*args, **kwargs)
|
||||
r["result"] = r["result"][:2]
|
||||
return r
|
||||
|
||||
|
||||
def build_scheduled_prompts(graph, schedules, clip):
|
||||
@@ -245,32 +238,11 @@ def build_scheduled_prompts(graph, schedules, clip):
|
||||
return {"result": (node.out(0),), "expand": g}
|
||||
|
||||
|
||||
def cache_key_from_inputs(cachekey, text, tags="", start=0.0, end=1.0, **kwargs):
|
||||
schedules = parse_prompt_schedules(text, filters=tags, start=start, end=end)
|
||||
def cache_key_from_inputs(cachekey, text, tags="", start=0.0, end=1.0, num_steps=0, **kwargs):
|
||||
schedules = parse_prompt_schedules(text, filters=tags, start=start, end=end, num_steps=num_steps)
|
||||
return [(pct, s[cachekey]) for pct, s in schedules]
|
||||
|
||||
|
||||
class PCLazyTextEncode:
|
||||
CACHE_KEY = cache_key_prompt
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {"clip": ("CLIP", {"rawLink": True}), "text": ("STRING", {"multiline": True})},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("CONDITIONING",)
|
||||
OUTPUT_TOOLTIPS = ("A fully encoded and scheduled conditioning",)
|
||||
CATEGORY = "promptcontrol"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, clip, text, unique_id):
|
||||
schedules = parse_prompt_schedules(text)
|
||||
graph = GraphBuilder(f"{unique_id}-")
|
||||
return build_scheduled_prompts(graph, schedules, clip)
|
||||
|
||||
|
||||
class PCLazyTextEncodeAdvanced:
|
||||
CACHE_KEY = cache_key_prompt
|
||||
|
||||
@@ -282,6 +254,7 @@ class PCLazyTextEncodeAdvanced:
|
||||
"tags": ("STRING", {"default": ""}),
|
||||
"start": ("FLOAT", {"min": 0.0, "max": 1.0, "default": 0.0, "step": 0.01}),
|
||||
"end": ("FLOAT", {"min": 0.0, "max": 1.0, "default": 1.0, "step": 0.01}),
|
||||
"num_steps": ("INT", {"min": 0, "max": 10000, "default": 0, "step": 1}),
|
||||
},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
@@ -290,12 +263,23 @@ class PCLazyTextEncodeAdvanced:
|
||||
CATEGORY = "promptcontrol"
|
||||
FUNCTION = "apply"
|
||||
|
||||
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)
|
||||
def apply(self, clip, text, unique_id, tags="", start=0.0, end=1.0, num_steps=0):
|
||||
schedules = parse_prompt_schedules(text, filters=tags, start=start, end=end, num_steps=num_steps)
|
||||
graph = GraphBuilder(f"{unique_id}-")
|
||||
return build_scheduled_prompts(graph, schedules, clip)
|
||||
|
||||
|
||||
class PCLazyTextEncode(PCLazyTextEncodeAdvanced):
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {"clip": ("CLIP", {"rawLink": True}), "text": ("STRING", {"multiline": True})},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
CATEGORY = "promptcontrol"
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"PCLazyTextEncode": PCLazyTextEncode,
|
||||
"PCLazyTextEncodeAdvanced": PCLazyTextEncodeAdvanced,
|
||||
|
||||
@@ -143,7 +143,6 @@ class PCSetPCTextEncodeSettings:
|
||||
return {
|
||||
"required": {"clip": ("CLIP",)},
|
||||
"optional": {
|
||||
"steps": ("INT", {"default": 0, "min": 0, "max": 10000}),
|
||||
"mask_width": ("INT", {"default": 512, "min": 64, "max": 4096 * 4}),
|
||||
"mask_height": ("INT", {"default": 512, "min": 64, "max": 4096 * 4}),
|
||||
"sdxl_width": ("INT", {"default": 1024, "min": 0, "max": 4096 * 4}),
|
||||
@@ -163,7 +162,6 @@ class PCSetPCTextEncodeSettings:
|
||||
def apply(
|
||||
self,
|
||||
clip,
|
||||
steps=0,
|
||||
mask_width=512,
|
||||
mask_height=512,
|
||||
sdxl_width=1024,
|
||||
@@ -174,7 +172,6 @@ class PCSetPCTextEncodeSettings:
|
||||
sdxl_crop_h=0,
|
||||
):
|
||||
settings = {
|
||||
"steps": steps,
|
||||
"mask_width": mask_width,
|
||||
"mask_height": mask_height,
|
||||
"sdxl_width": sdxl_width,
|
||||
|
||||
+29
-15
@@ -103,13 +103,25 @@ def clamp(a, b, c):
|
||||
return min(max(a, b), c)
|
||||
|
||||
|
||||
def get_steps(tree):
|
||||
res = [100]
|
||||
def get_steps(tree, num_steps):
|
||||
res = [num_steps or 100]
|
||||
|
||||
def tostep(s):
|
||||
w = float(s) * 100
|
||||
w = int(clamp(0, w, 100))
|
||||
return w
|
||||
steps = num_steps or 100
|
||||
if "." in str(s) or not num_steps:
|
||||
w = float(s)
|
||||
value = w * steps
|
||||
else:
|
||||
w = int(s)
|
||||
value = w
|
||||
|
||||
if w > 1 and not num_steps:
|
||||
log.warning(
|
||||
"You haven't configured the number of steps for Prompt Control to use, %s will be clipped to 1.0", w
|
||||
)
|
||||
value = steps
|
||||
|
||||
return int(clamp(0, value, steps))
|
||||
|
||||
class CollectSteps(lark.Visitor):
|
||||
def scheduled(self, tree):
|
||||
@@ -131,15 +143,14 @@ def get_steps(tree):
|
||||
def sequence(self, tree):
|
||||
steps = tree.children[1::2]
|
||||
for i, steps in enumerate(steps):
|
||||
w = float(tree.children[i * 2 + 1]) * 100
|
||||
tree.children[i * 2 + 1] = clamp(0, w, 100)
|
||||
w = tostep(tree.children[i * 2 + 1])
|
||||
tree.children[i * 2 + 1] = w
|
||||
res.append(w)
|
||||
|
||||
def alternate(self, tree):
|
||||
step_size = int(round(float(tree.children[-1] or 0.1), 2) * 100)
|
||||
step_size = clamp(1, step_size, 100)
|
||||
step_size = tostep(round(float(tree.children[-1] or 0.1), 2))
|
||||
tree.children[-1] = step_size
|
||||
res.extend([x for x in range(step_size, 100, step_size)])
|
||||
res.extend([x for x in range(step_size, num_steps or 100, step_size)])
|
||||
|
||||
CollectSteps().visit(tree)
|
||||
|
||||
@@ -259,29 +270,31 @@ def at_step(step, filters, tree):
|
||||
|
||||
|
||||
class PromptSchedule(object):
|
||||
def __init__(self, prompt, filters="", start=0.0, end=1.0):
|
||||
# 0 num_steps means unconfigured
|
||||
def __init__(self, prompt, filters="", start=0.0, end=1.0, num_steps=0):
|
||||
self.filters = filters
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.num_steps = num_steps
|
||||
self.prompt = prompt.strip()
|
||||
self.defaults = {}
|
||||
self.loaded_loras = {}
|
||||
|
||||
self.parsed_prompt = self._parse()
|
||||
self.parsed_prompt = self._parse(num_steps)
|
||||
|
||||
def __iter__(self):
|
||||
# Filter out zero, it's only useful for interpolation
|
||||
return (x for x in self.parsed_prompt if x[0] != 0)
|
||||
|
||||
def _parse(self):
|
||||
def _parse(self, num_steps):
|
||||
filters = [x.strip() for x in self.filters.upper().split(",")]
|
||||
try:
|
||||
parsed = []
|
||||
tree = prompt_parser.parse(self.prompt)
|
||||
steps = get_steps(tree)
|
||||
steps = get_steps(tree, num_steps=num_steps)
|
||||
|
||||
def f(x):
|
||||
return round(x / 100, 2)
|
||||
return round(x / (num_steps or 100), 2)
|
||||
|
||||
for t in steps:
|
||||
p = at_step(t, filters, tree)
|
||||
@@ -331,6 +344,7 @@ class PromptSchedule(object):
|
||||
filters=ifspecified(filters, self.filters),
|
||||
start=ifspecified(start, self.start),
|
||||
end=ifspecified(end, self.end),
|
||||
num_steps=self.num_steps,
|
||||
)
|
||||
return p
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import unittest
|
||||
import unittest.mock as mock
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
|
||||
def find_file(name):
|
||||
@@ -7,10 +10,17 @@ def find_file(name):
|
||||
return names.get(name)
|
||||
|
||||
|
||||
def apply(cls, text, **kwargs):
|
||||
model = [0, 1]
|
||||
clip = [0, 0]
|
||||
return cls().apply(unique_id="UID", model=model, clip=clip, text=text, **kwargs)
|
||||
|
||||
|
||||
@mock.patch("prompt_control.utils.lora_name_to_file", find_file)
|
||||
@mock.patch.dict("sys.modules", nodes=mock.MagicMock())
|
||||
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
|
||||
@@ -73,23 +83,16 @@ class GraphTests(unittest.TestCase):
|
||||
|
||||
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"
|
||||
]
|
||||
with self.assertLogs(log, level="WARNING") as cm:
|
||||
result = apply(PCLazyLoraLoader, "prompt here <lora:nonexistent:1.0:0.5>")["expand"]
|
||||
result_adv = apply(PCLazyLoraLoaderAdvanced, "prompt here <lora:nonexistent:1.0:0.5>")["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"]
|
||||
result = apply(PCLazyLoraLoader, "<lora:test:1>")["expand"]
|
||||
result2 = apply(PCLazyLoraLoader, "prompt here <lora:test:1.0:0.5><lora:test:0:0.5>")["expand"]
|
||||
result3 = apply(PCLazyLoraLoaderAdvanced, "prompt here <lora:test:1.0:0.5><lora:test:0:0.5>")["expand"]
|
||||
self.assertEqual(result, result2)
|
||||
self.assertEqual(result2, result3)
|
||||
self.assertEqual(
|
||||
@@ -107,7 +110,7 @@ class GraphTests(unittest.TestCase):
|
||||
}
|
||||
},
|
||||
)
|
||||
result = PCLazyLoraLoader().apply(model, clip, "<lora:test:1><lora:other:0.5>", "UID")["expand"]
|
||||
result = apply(PCLazyLoraLoader, "<lora:test:1><lora:other:0.5>")["expand"]
|
||||
self.assertEqual(
|
||||
result,
|
||||
{
|
||||
@@ -134,7 +137,7 @@ class GraphTests(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
result = PCLazyLoraLoader().apply(model, clip, "prompt here <lora:test:1.0:0.5>", "UID")["expand"]
|
||||
result = apply(PCLazyLoraLoader, "prompt here <lora:test:1.0:0.5>")["expand"]
|
||||
self.assertEqual(
|
||||
result,
|
||||
{
|
||||
@@ -151,8 +154,8 @@ class GraphTests(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
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"]
|
||||
result = apply(PCLazyLoraLoader, "prompt [<lora:test:0.5>:0.5]")["expand"]
|
||||
result2 = apply(PCLazyLoraLoaderAdvanced, "prompt [<lora:test:0.5>:0.5]")["expand"]
|
||||
self.assertEqual(result, result2)
|
||||
expected = {
|
||||
"UID-1": {
|
||||
@@ -186,9 +189,7 @@ class GraphTests(unittest.TestCase):
|
||||
},
|
||||
}
|
||||
self.assertEqual(result, expected)
|
||||
result2 = PCLazyLoraLoaderAdvanced().apply(model, clip, "prompt [<lora:test:0.5>:0.5]", "UID", start=0.6)[
|
||||
"expand"
|
||||
]
|
||||
result2 = apply(PCLazyLoraLoaderAdvanced, "prompt [<lora:test:0.5>:0.5]", start=0.6)["expand"]
|
||||
self.assertEqual(
|
||||
result2,
|
||||
{
|
||||
|
||||
+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.9"
|
||||
version = "2.0.0-beta.11"
|
||||
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