Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fbae90478 | ||
|
|
2f12069821 | ||
|
|
3baeabb8ee | ||
|
|
c85134b31e | ||
|
|
26f7e1ff24 | ||
|
|
e9e8b75d7f | ||
|
|
01526e3923 | ||
|
|
e888238625 | ||
|
|
53a6d48cb1 | ||
|
|
a0df992741 | ||
|
|
be51e0dfc4 | ||
|
|
c023956b4c | ||
|
|
b33f24e0cb | ||
|
|
a637321356 | ||
|
|
3a2d08fcf7 | ||
|
|
99d966d74a | ||
|
|
724488d20b |
@@ -20,5 +20,7 @@ A clear and concise description of what the bug is.
|
||||
Information needed to trigger the problem.
|
||||
If possible, attach a workflow to reproduce the problem
|
||||
|
||||
If a workflow works, but isn't producing the correct output, please enable debug logging with the `PCSetLogLevel` node (from `promptcontrol/tools`) and run your workflow with debug logging enabled, and copy the outputs here.
|
||||
|
||||
**Expected behavior**
|
||||
A description of what you expected to happen.
|
||||
|
||||
@@ -62,6 +62,8 @@ Then restart ComfyUI afterwards.
|
||||
|
||||
# Core nodes
|
||||
|
||||
**Note**: The documentation refers to the nodes with their internal names for consistency. The display name may change, but ComfyUI's search will always find the nodes with the internal name. `PCLazyTextEncode` and `PCLazyLoraLoader` are the main ones you'll want to use, also known as `PC: Schedule Prompt` and `PC: Schedule LoRas`.
|
||||
|
||||
## PCLazyTextEncode and PCLazyTextEncodeAdvanced
|
||||
|
||||
`PCLazyTextEncode` uses ComfyUI's lazy graph execution mechanism to generate a graph of `PCTextEncode` and `SetConditioningTimestepRange` nodes from a prompt with schedules. This has the advantage that if a part of the schedule doesn't change, ComfyUI's caching mechanism allows you to avoid re-encoding the non-changed part.
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ These functions are applied to each prompt chunk **after** `BREAK`, `AND` etc. h
|
||||
|
||||
Multiple instances of these functions are applied in the order they appear in the prompt.
|
||||
|
||||
**NOTE:** These functions are *not* smart about syntax and will break emphasis if the separator occurs inside parentheses. I might fix this at some point, but for now, keep this in mind.
|
||||
**NOTE** To avoid breaking emphasis syntax, the functions ignore any separators inside parentheses
|
||||
|
||||
For example:
|
||||
- `SHIFT(1) cat, dog, tiger, mouse` does a shift and results in `dog, tiger, mouse, cat`. (whitespace may vary)
|
||||
|
||||
@@ -4,6 +4,29 @@ from .prompts import encode_prompt
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
|
||||
class PCTextEncodeWithRange:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {"clip": ("CLIP",), "text": ("STRING", {"multiline": True})},
|
||||
"optional": {
|
||||
"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}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("CONDITIONING",)
|
||||
CATEGORY = "promptcontrol/tools"
|
||||
FUNCTION = "apply"
|
||||
DESCRIPTION = "Like PCTextEncode, but if you know the range you need for a prompt, can be slightly more efficient when you have LoRAs scheduled on a CLIP model"
|
||||
|
||||
def apply(self, clip, text, start=0.0, end=1.0):
|
||||
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)
|
||||
return (encode_prompt(clip, text, start, end, defaults, masks),)
|
||||
|
||||
|
||||
class PCTextEncode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -14,17 +37,15 @@ class PCTextEncode:
|
||||
RETURN_TYPES = ("CONDITIONING",)
|
||||
CATEGORY = "promptcontrol"
|
||||
FUNCTION = "apply"
|
||||
DESCRIPTION = "Encodes a prompt with extra goodies from Prompt Control. This node does *not* support scheduling"
|
||||
|
||||
def apply(self, clip, text):
|
||||
defaults = clip.patcher.model_options.get("x-promptcontrol.defaults", {})
|
||||
masks = clip.patcher.model_options.get("x-promptcontrol.masks", None)
|
||||
return (encode_prompt(clip, text, 0, 1.0, defaults, masks),)
|
||||
return PCTextEncodeWithRange.apply(self, clip, text, 0.0, 1.0)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"PCTextEncode": PCTextEncode,
|
||||
}
|
||||
NODE_CLASS_MAPPINGS = {"PCTextEncode": PCTextEncode, "PCTextEncodeWithRange": PCTextEncodeWithRange}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"PCTextEncode": "PC Text Encode (no scheduling)",
|
||||
"PCTextEncode": "PC: Text Encode (no scheduling)",
|
||||
"PCTextEncodeWithRange": "PC: Text Encode with Range (no scheduling)",
|
||||
}
|
||||
|
||||
@@ -84,5 +84,5 @@ NODE_CLASS_MAPPINGS = {
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"PCLoraHooksFromText": "PC LoRA Hooks From Text (non-lazy)",
|
||||
"PCLoraHooksFromText": "PC: LoRA Hooks From Text (non-lazy)",
|
||||
}
|
||||
|
||||
@@ -7,15 +7,24 @@ from .prompts import get_function
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
from .utils import consolidate_schedule, find_nonscheduled_loras
|
||||
import json
|
||||
|
||||
|
||||
def cache_key_hack(inputs):
|
||||
def _cache_key(cachekey, inputs):
|
||||
out = inputs.copy()
|
||||
if not is_link(inputs["text"]):
|
||||
out["text"] = cache_key_from_inputs(**inputs)
|
||||
out["text"] = cache_key_from_inputs(cachekey, **inputs)
|
||||
return out
|
||||
|
||||
|
||||
def cache_key_prompt(inputs):
|
||||
return _cache_key("prompt", inputs)
|
||||
|
||||
|
||||
def cache_key_lora(inputs):
|
||||
return _cache_key("loras", inputs)
|
||||
|
||||
|
||||
def create_lora_loader_nodes(graph, model, clip, loras):
|
||||
for path, info in loras.items():
|
||||
log.info("Creating LoraLoader for %s", path)
|
||||
@@ -34,7 +43,7 @@ def create_hook_nodes_for_lora(graph, path, info, existing_node, start_pct, end_
|
||||
prev_keyframe = None
|
||||
next_keyframe = None
|
||||
if not existing_node:
|
||||
log.debug("Creating hook for %s", path)
|
||||
log.debug("Creating hook for %s, weight=%s, weight_clip=%s", path, info["weight"], info["weight_clip"])
|
||||
hook_node = graph.node("CreateHookLora")
|
||||
hook_node.set_input("lora_name", path)
|
||||
hook_node.set_input("strength_model", info["weight"])
|
||||
@@ -67,7 +76,7 @@ def create_hook_nodes_for_lora(graph, path, info, existing_node, start_pct, end_
|
||||
next_keyframe.set_input("strength_mult", 1.0)
|
||||
prev_hook_kf = next_keyframe.out(0)
|
||||
if end_pct < 1.0:
|
||||
log.debug("Creating end keyframe for %s, start=%s", path, start_pct)
|
||||
log.debug("Creating end keyframe for %s, start=%s", path, end_pct)
|
||||
next_keyframe = graph.node("CreateHookKeyframe")
|
||||
next_keyframe.set_input("strength_mult", 0.0)
|
||||
next_keyframe.set_input("start_percent", end_pct)
|
||||
@@ -108,7 +117,7 @@ def build_lora_schedule(graph, schedule, model, clip, apply_hooks=True, return_h
|
||||
# Finally, combine all hooks and optionally apply
|
||||
if len(hooks) > 0:
|
||||
res = hooks[0]
|
||||
for h in hooks[:1]:
|
||||
for h in hooks[1:]:
|
||||
n = graph.node("CombineHooks2")
|
||||
n.set_input("hooks_A", res.out(0))
|
||||
n.set_input("hooks_B", h.out(0))
|
||||
@@ -123,6 +132,7 @@ def build_lora_schedule(graph, schedule, model, clip, apply_hooks=True, return_h
|
||||
clip = n.out(0)
|
||||
|
||||
r = graph.finalize()
|
||||
log.debug("LazyLoraLoader built graph: %s", json.dumps(r))
|
||||
|
||||
if return_hooks:
|
||||
ret = (model, clip, res)
|
||||
@@ -133,7 +143,7 @@ def build_lora_schedule(graph, schedule, model, clip, apply_hooks=True, return_h
|
||||
|
||||
|
||||
class PCLazyLoraLoaderAdvanced:
|
||||
CACHE_KEY = cache_key_hack
|
||||
CACHE_KEY = cache_key_lora
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -164,7 +174,7 @@ class PCLazyLoraLoaderAdvanced:
|
||||
|
||||
|
||||
class PCLazyLoraLoader:
|
||||
CACHE_KEY = cache_key_hack
|
||||
CACHE_KEY = cache_key_lora
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -194,7 +204,6 @@ class PCLazyLoraLoader:
|
||||
def build_scheduled_prompts(graph, schedules, clip):
|
||||
nodes = []
|
||||
start_pct = 0.0
|
||||
prompt_cache = {}
|
||||
for end_pct, c in schedules:
|
||||
p = c["prompt"]
|
||||
p, classnames = get_function(p, "NODE", ["PCTextEncode", "text"])
|
||||
@@ -203,12 +212,9 @@ def build_scheduled_prompts(graph, schedules, clip):
|
||||
if classnames:
|
||||
classname = classnames[0][0]
|
||||
paramname = classnames[0][1]
|
||||
node = prompt_cache.get((p, classname, paramname))
|
||||
if not node:
|
||||
node = graph.node(classname)
|
||||
node.set_input("clip", clip)
|
||||
node.set_input(paramname, p)
|
||||
prompt_cache[(p, classname, paramname)] = node
|
||||
node = graph.node(classname)
|
||||
node.set_input("clip", clip)
|
||||
node.set_input(paramname, p)
|
||||
timestep = graph.node("ConditioningSetTimestepRange")
|
||||
timestep.set_input("conditioning", node.out(0))
|
||||
timestep.set_input("start", start_pct)
|
||||
@@ -223,17 +229,18 @@ def build_scheduled_prompts(graph, schedules, clip):
|
||||
node = combiner
|
||||
|
||||
g = graph.finalize()
|
||||
log.debug("Built graph: %s", json.dumps(g))
|
||||
|
||||
return {"result": (node.out(0),), "expand": g}
|
||||
|
||||
|
||||
def cache_key_from_inputs(text, tags="", start=0.0, end=1.0, **kwargs):
|
||||
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)
|
||||
return [(pct, s["prompt"]) for pct, s in schedules]
|
||||
return [(pct, s[cachekey]) for pct, s in schedules]
|
||||
|
||||
|
||||
class PCLazyTextEncode:
|
||||
CACHE_KEY = cache_key_hack
|
||||
CACHE_KEY = cache_key_prompt
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -253,7 +260,7 @@ class PCLazyTextEncode:
|
||||
|
||||
|
||||
class PCLazyTextEncodeAdvanced:
|
||||
CACHE_KEY = cache_key_hack
|
||||
CACHE_KEY = cache_key_prompt
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
|
||||
@@ -3,6 +3,32 @@ import logging
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
|
||||
class PCSetLogLevel:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"clip": ("CLIP",),
|
||||
},
|
||||
"optional": {
|
||||
"level": (["INFO", "DEBUG", "WARNING", "ERROR"], {"default": "INFO"}),
|
||||
},
|
||||
}
|
||||
|
||||
def apply(self, clip, level="INFO"):
|
||||
log.setLevel(getattr(logging, level))
|
||||
log.info("Set logging level to %s", level)
|
||||
return (clip,)
|
||||
|
||||
RETURN_TYPES = ("CLIP",)
|
||||
CATEGORY = "promptcontrol/tools"
|
||||
DESCRIPTION = (
|
||||
"A debug node to configure Prompt Control logging level. Pass a CLIP through it before you run any PC nodes"
|
||||
)
|
||||
|
||||
FUNCTION = "apply"
|
||||
|
||||
|
||||
class PCAddMaskToCLIP:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -14,7 +40,7 @@ class PCAddMaskToCLIP:
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("CLIP",)
|
||||
CATEGORY = "promptcontrol/v2"
|
||||
CATEGORY = "promptcontrol/tools"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, clip, mask=None):
|
||||
@@ -35,7 +61,7 @@ class PCAddMaskToCLIPMany:
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("CLIP",)
|
||||
CATEGORY = "promptcontrol/v2"
|
||||
CATEGORY = "promptcontrol/tools"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, clip, mask1=None, mask2=None, mask3=None, mask4=None):
|
||||
@@ -65,7 +91,7 @@ class PCSetPCTextEncodeSettings:
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("CLIP",)
|
||||
CATEGORY = "promptcontrol/v2"
|
||||
CATEGORY = "promptcontrol/tools"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(
|
||||
@@ -101,10 +127,12 @@ NODE_CLASS_MAPPINGS = {
|
||||
"PCSetPCTextEncodeSettings": PCSetPCTextEncodeSettings,
|
||||
"PCAddMaskToCLIP": PCAddMaskToCLIP,
|
||||
"PCAddMaskToCLIPMany": PCAddMaskToCLIPMany,
|
||||
"PCSetLogLevel": PCSetLogLevel,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"PCSetTextEncodeSettings": "PC: Configure PCTextEncode",
|
||||
"PCSetPCTextEncodeSettings": "PC: Configure PCTextEncode",
|
||||
"PCAddMaskToCLIP": "PC: Attach Mask",
|
||||
"PCAddMaskToCLIPMany": "PC: Attach Mask (multi)",
|
||||
"PCSetLogLevel": "PC: Configure Logging (for debug)",
|
||||
}
|
||||
|
||||
@@ -8,7 +8,15 @@ log = logging.getLogger("comfyui-prompt-control")
|
||||
from functools import lru_cache
|
||||
|
||||
if lark.__version__ == "0.12.0":
|
||||
x = "Your lark package reports an ancient version (0.12.0) and will not work. If you have the 'lark-parser' package in your Python environment, remove that and *reinstall* lark!"
|
||||
from sys import executable
|
||||
|
||||
x = "\n".join(
|
||||
[
|
||||
"Your lark package reports an ancient version (0.12.0) and will not work. If you have the 'lark-parser' package in your Python environment, remove that and *reinstall* lark!",
|
||||
f"{executable} -m pip uninstall lark-parser lark",
|
||||
f"{executable} -m pip install lark",
|
||||
]
|
||||
)
|
||||
log.error(x)
|
||||
raise ImportError(x)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import torch
|
||||
from functools import partial
|
||||
from comfy_extras.nodes_mask import FeatherMask, MaskComposite
|
||||
|
||||
from .utils import safe_float, get_function, parse_floats
|
||||
from .utils import safe_float, get_function, parse_floats, smarter_split
|
||||
from .adv_encode import advanced_encode_from_tokens
|
||||
from .cutoff import process_cuts
|
||||
from .parser import parse_cuts
|
||||
@@ -88,8 +88,9 @@ def shuffle_chunk(shuffle, c):
|
||||
"separator": separator,
|
||||
}.get(joiner, joiner)
|
||||
|
||||
log.info("%s arg=%s sep=%s join=%s", func, shuffle_count, separator, joiner)
|
||||
separated = c.split(separator)
|
||||
log.debug("%s arg=%s sep=%s join=%s", func, shuffle_count, separator, joiner)
|
||||
separated = smarter_split(separator, c)
|
||||
log.debug("Prompt split into %s", separated)
|
||||
if func == "SHIFT":
|
||||
shuffle_count = shuffle_count % len(separated)
|
||||
permutation = separated[shuffle_count:] + separated[:shuffle_count]
|
||||
|
||||
+23
-1
@@ -2,7 +2,9 @@ from pathlib import Path
|
||||
import re
|
||||
import logging
|
||||
|
||||
import folder_paths
|
||||
# Allow testing
|
||||
if __name__ != "__main__":
|
||||
import folder_paths
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
@@ -47,6 +49,26 @@ def find_nonscheduled_loras(consolidated_schedule):
|
||||
return {k: v for (k, v) in candidate_loras.items() if k not in to_remove}
|
||||
|
||||
|
||||
def smarter_split(separator, string):
|
||||
"""Does not break () when splitting"""
|
||||
splits = []
|
||||
prev = 0
|
||||
stack = 0
|
||||
escape = False
|
||||
for idx, x in enumerate(string):
|
||||
if x == "(" and not escape:
|
||||
stack += 1
|
||||
elif x == ")" and not escape:
|
||||
stack = max(0, stack - 1)
|
||||
elif x == separator and stack == 0:
|
||||
splits.append(string[prev:idx])
|
||||
prev = idx + 1
|
||||
escape = x == "\\"
|
||||
|
||||
splits.append(string[prev : idx + 1])
|
||||
return splits
|
||||
|
||||
|
||||
def find_closing_paren(text, start):
|
||||
stack = 1
|
||||
for i, char in enumerate(text[start:]):
|
||||
|
||||
+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.3"
|
||||
version = "2.0.0-beta.4"
|
||||
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