Initial commit

This commit is contained in:
asagi4
2023-11-02 23:05:13 +02:00
commit 69d4eadfab
6 changed files with 286 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
all: check format
@echo "Done"
check:
pyflakes *.py */*.py
format:
black -l 120 *.py */*.py
.PHONY: check format all
+31
View File
@@ -0,0 +1,31 @@
# ComfyUI utility nodes
A collection of miscellaneous utility nodes for ComfyUI
# Nodes
## MU_JinjaRender
You can use this node to evaluate a string as a Jinja2 template. Note, however, that because ComfyUI's frontend uses `{}` for syntax, There are the following modifications to Jinja syntax:
- `{% %}` becomes `<% %>`
- `{{ }}` becomes `<= =>`
- `{# #}` becomes `<# #>`
### Functions in Jinja templates
The following functions and constants are available:
- `pi`
- `min`, `max`, `clamp(minimum, value, maximum)`,
- `abs`, `round`, `ceil`, `floor`
- `sqrt` `sin`, `cos`, `tan`, `asin`, `acos`, `atan`. These functions are rounded to two decimals
In addition, a special `steps` function exists.
The `steps` function will generate a list of steps for iterating.
You can call it either as `steps(end)`, `steps(end, step=0.1)` or `steps(start, end, step)`. `step` is an optional parameter that defaults to `0.1`. It'll return steps *inclusive* of start and end as long as step doesn't go past the end.
The second form is equivalent to `steps(step, end, step)`. i.e. it starts at the first step.
+10
View File
@@ -0,0 +1,10 @@
from .utils import jinja_render as j
from .utils import wildcards as w
from .utils import other as o
NODE_CLASS_MAPPINGS = {
"MUJinjaRender": j.MUJinjaRender,
"MUSimpleWildcard": w.MUSimpleWildcard,
"MUConditioningCutoff": o.MUConditioningCutoff,
"MUStringConcat": o.MUStringConcat,
}
+83
View File
@@ -0,0 +1,83 @@
import logging
import math
import re
from datetime import datetime
log = logging.getLogger("comfyui-misc-utils")
def steps(start, end=None, step=0.1):
if end is None:
end = start
start = step
while start <= end:
yield start
start += step
start = round(start, 2)
def template(template, sequence, *funcs):
funcs = [lambda x: x] + list(*funcs)
res = []
for item in sequence:
x = template
for i, f in enumerate(funcs):
x = x.replace(f"${i}", str(f(i)))
res.append(x)
return "".join(res)
def clamp(a, b, c):
return max(a, min(b, c))
JINJA_ENV = {
"pi": math.pi,
"floor": math.floor,
"ceil": math.ceil,
"min": min,
"max": max,
"abs": abs,
"clamp": clamp,
"round": round,
"template": template,
"steps": steps,
"datetime": datetime,
}
for fname in ["sqrt", "sin", "cos", "tan", "asin", "acos", "atan"]:
f = getattr(math, fname)
JINJA_ENV[fname] = lambda x: round(f(x), 2)
def render_jinja(text):
from jinja2 import Environment
jenv = Environment(
block_start_string="<%",
block_end_string="%>",
variable_start_string="<=",
variable_end_string="=>",
comment_start_string="<#",
comment_end_string="#>",
)
return jenv.from_string(text, globals=JINJA_ENV).render()
class MUJinjaRender:
@classmethod
def INPUT_TYPES(s):
return {"required": {"text": ("STRING", {"default": "", "multiline": True})}}
RETURN_TYPES = ("STRING",)
CATEGORY = "promptcontrol/tools"
FUNCTION = "render"
def render(self, text):
t = render_jinja(text)
if t.strip() != text.strip():
log.info("Jinja render result: %s", re.sub("\s+", " ", t, flags=re.MULTILINE))
return (t,)
+50
View File
@@ -0,0 +1,50 @@
class MUStringConcat:
@classmethod
def INPUT_TYPES(s):
t = ("STRING", {"default": ""})
return {
"optional": {
"string1": t,
"string2": t,
"string3": t,
"string4": t,
}
}
RETURN_TYPES = ("STRING",)
CATEGORY = "miscutils"
FUNCTION = "cat"
def cat(self, string1="", string2="", string3="", string4=""):
return string1 + string2 + string3 + string4
class MUConditioningCutoff:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"conds": ("CONDITIONING",),
"cutoff": ("FLOAT", {"min": 0.00, "max": 1.00, "default": 0.0, "step": 0.01}),
}
}
RETURN_TYPES = ("CONDITIONING",)
CATEGORY = "miscutils"
FUNCTION = "apply"
def apply(self, conds, cutoff):
res = []
new_start = 1.0
for c in conds:
end = c[1].get("end_percent", 0.0)
if 1.0 - end < cutoff:
continue
c = [c[0].clone(), c[1].copy()]
c[1]["start_percent"] = new_start
c[1]["end_percent"] = end
new_start = end
res.append(c)
return (res,)
+104
View File
@@ -0,0 +1,104 @@
import logging
from os import environ
import random
import re
from pathlib import Path
from server import PromptServer
log = logging.getLogger("comfyui-misc-utils")
CLASS_NAME = "MUSimpleWildcard"
def wildcard_prompt_handler(json_data):
log.info("Resolving wildcards...")
for node_id in json_data["prompt"].keys():
if json_data["prompt"][node_id]["class_type"] == CLASS_NAME:
handle_wildcard_node(json_data, node_id)
return json_data
def handle_wildcard_node(json_data, node_id):
wildcard_info = json_data.get("extra_data", {}).get("extra_pnginfo", {}).get(CLASS_NAME, {})
n = json_data["prompt"][node_id]
if not (n["inputs"].get("use_pnginfo") and node_id in wildcard_info):
text = MUSimpleWildcard.select(n["inputs"]["text"], n["inputs"]["seed"])
if text.strip() != n["inputs"]["text"].strip():
json_data["prompt"][node_id]["inputs"]["use_pnginfo"] = True
wildcard_info[node_id] = text
json_data["extra_data"]["extra_pnginfo"][CLASS_NAME] = wildcard_info
return json_data
PromptServer.instance.add_on_prompt_handler(wildcard_prompt_handler)
def variable_substitution(text):
var_re = re.compile(r"(\$[a-z]+)\s*=([^;\n]*);?")
m = var_re.search(text)
while m:
var = m[1]
sub = m[2]
s, e = m.span()
text = text[:s] + text[e:]
log.info("Substituting %s with '%s'", var, sub)
text = text.replace(var, sub)
m = var_re.search(text)
return text
class MUSimpleWildcard:
RAND = random.Random()
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"default": ""}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
},
"optional": {"use_pnginfo": ("BOOLEAN", {"default": False})},
"hidden": {
"extra_pnginfo": "EXTRA_PNGINFO",
"unique_id": "UNIQUE_ID",
},
}
RETURN_TYPES = ("STRING",)
CATEGORY = "promptcontrol/tools"
FUNCTION = "doit"
@classmethod
def read_wildcards(cls, name):
path = environ.get("MU_WILDCARD_BASEDIR", "wildcards")
f = (Path(path) / Path(name)).with_suffix(".txt")
try:
with open(f, "r") as file:
return [l.strip() for l in file.readlines() if l.strip()]
except:
return [name]
@classmethod
def select(cls, text, seed):
cls.RAND.seed(seed)
matches = re.findall(r"(\$([A-Za-z0-9_/.-]+)(\+[0-9]+)?\$)", text)
for placeholder, wildcard, offset in matches:
if offset:
offset = int(offset[1:])
cls.RAND.seed(seed + offset)
w = cls.RAND.choice(cls.read_wildcards(wildcard))
text = text.replace(placeholder, w, 1)
log.info("Selected wildcard %s for %s", w, placeholder)
if offset:
cls.RAND.seed(seed)
return text
def doit(self, text, seed, extra_pnginfo, unique_id, use_pnginfo=False):
if use_pnginfo and unique_id in extra_pnginfo.get(CLASS_NAME, {}):
text = extra_pnginfo[CLASS_NAME][unique_id]
log.info("MUSimpleWildcard using prompt: %s", text)
text = variable_substitution(text)
return (text,)