initial commit

This commit is contained in:
Heinz Ulrich Stille
2023-08-14 11:01:47 +02:00
commit 1a9adf21bb
7 changed files with 380 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
# hus' utils for ComfyUI
Some nodes I cobbled together to satisfy my preferences, to recreate some behaviours of A1111.
## Nodes
### Fetch widget value
Extracted from 'Math Expression' from [ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts).
Fetch the value of widget _widget_name_ from node _node_name_. Node name can be type, title or S&R name. If _multiple_ is 'no', use first matching node, if it is 'yes', return all as concatenated string separated by ', '.
### 3way Prompt Styler
Adapted from [Load Styles CSV](https://github.com/theUpsider/ComfyUI-Styles_CSV_Loader). Positive split into G and L for SDXL.
Load prompt styles from a file named 'styles.csv' in the source directory of this node (usually .../ComfyUI/custom_nodes/ComfyUI_hus_utils/). Each line has four columns: _style name_, _prompt G_, _prompt L_ and _negative prompt_. L and negative are simply appended to the respective inputs, the value of G is searched for '{prompt}', which is replaced with the G input.
Example provided in _example styles.csv_
### Text Hash
Return the first _length_ characters of the sha256 digest of the input.
### Date Time Format
Return the current date and/or time as formatted by strftime using _format_.
### Batch State
Check whether the state of the workflow (inputs and linking) has changed.
- _changed_ returns 1 if a change is detected, 0 otherwise
- _hash_ returns a hash of the current state
- _count_ counts up every time the state is unchanged and resets to 0 on change
Add _count_ to a seed input with 'fixed' value to create a sequence of images with consecutive seeds every time 'Queue Prompt' is clicked. When any input is changed, the sequence starts again from the value of the seed node. The purpose is to check the influence of changes in prompt, CFG, steps etc on the same set of images.
If the seed node is set to 'randomize' or anything else except 'fixed', it's input value will change every time, causing _count_ to remain at 0, which should cause the sum to show the expected behaviour.
### Debug Extra
Expose some of the internal workings for developing custom nodes.
## Example
Example workflow based on [Sytan SDXL ComfyUI](https://github.com/SytanSD/Sytan-SDXL-ComfyUI)
- Filename prefixes are created as _prompt hash_-_prompt style_-_seed_-_model name_-_base {sampler, scheduler, steps, cfg}_-_refiner {sampler, scheduler, steps, cfg}_
- Seeds are generated in a fixed sequence which restarts every time any input is changed.
### Dependencies
Nodes from ...
- [WAS Node Suite](https://github.com/WASasquatch/was-node-suite-comfyui)
- [ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts)
- [tinyterraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes)
- and probably some more I forgot ...
+25
View File
@@ -0,0 +1,25 @@
from .text import *
from .state import *
from .style import *
from .debug import *
NODE_CLASS_MAPPINGS = {
"Fetch widget value": FetchNodeValue,
"3way Prompt Styler": PromptStylerCSV3Way,
"Text Hash": TextHash,
"Date Time Format": DateTimeFormat,
"Batch State": BatchState,
"Debug Extra": DebugExtra,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"FetchNodeValue": "Fetch Widget Value",
"PromptStylerCSV3Way": "3way Prompt Styler",
"TextHash": "Text Hash",
"DateTimeFormat": "Date Time Format",
"BatchState": "Batch State",
"DebugExtra": "Debug Extra",
}
print("\033[34mhus' utility nodes: \033[92mloaded\033[0m")
+60
View File
@@ -0,0 +1,60 @@
import pprint
import collections
class DebugExtra:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
},
"hidden": {"extra_pnginfo": "EXTRA_PNGINFO",
"prompt": "PROMPT"},
}
RETURN_TYPES = ("STRING", "STRING", "STRING", )
RETURN_NAMES = ("extra_pnginfo", "prompt", "state", )
FUNCTION = "debug_out"
CATEGORY = "utils/hus"
def debug_out(self, extra_pnginfo, prompt):
workflow = extra_pnginfo["workflow"]
results = {}
for node in workflow["nodes"]:
node_id = str(node["id"])
name = node["type"]
if "Debug" in name or "Show" in name or "Function" in name or "Evaluate" in name:
continue
if "title" in node:
name += "("+node["title"]+")"
else:
if "properties" in node:
if "Node name for S&R" in node["properties"]:
name += "("+node["properties"]["Node name for S&R"]+")"
name += "."+node_id
if "widgets_values" in node and "inputs" not in node:
results[name] = node["widgets_values"]
elif node_id in prompt:
values = prompt[node_id]
if "inputs" in values:
results[name] = {}
for widget in values["inputs"].items():
(n, v) = widget
if type(v) is not str and isinstance(v, collections.abc.Sequence):
continue
results[name][n] = v
elif "widgets_values" in node:
results[name] = node["widgets_values"]
else:
results[name] = "no widget values"
return (pprint.pformat(extra_pnginfo), pprint.pformat(prompt), pprint.pformat(results), )
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
+6
View File
@@ -0,0 +1,6 @@
name,positive prompt (g),supporting terms (l),negative_prompt
none,,,
base,{prompt},,"text, watermark, bad hands, bad anatomy, missing limbs, additional limbs, mutated, ugly, sloppy"
generic,breathtaking {prompt},"professional, highly detailed","abstract glitch, blurry"
Photography,cinematic photograph of {prompt},"bokeh, f1.8, 35mm photograph, film, shallow depth of field, professional, highly detailed","noise, grit, blurry, malformed, warped, deformed"
Oil Painting,beautiful oil painting of,"professional, art, detailed","photo, photorealistic, photographic, deformed, realism, ugly"
1 name positive prompt (g) supporting terms (l) negative_prompt
2 none
3 base {prompt} text, watermark, bad hands, bad anatomy, missing limbs, additional limbs, mutated, ugly, sloppy
4 generic breathtaking {prompt} professional, highly detailed abstract glitch, blurry
5 Photography cinematic photograph of {prompt} bokeh, f1.8, 35mm photograph, film, shallow depth of field, professional, highly detailed noise, grit, blurry, malformed, warped, deformed
6 Oil Painting beautiful oil painting of professional, art, detailed photo, photorealistic, photographic, deformed, realism, ugly
+123
View File
@@ -0,0 +1,123 @@
import hashlib
import collections
import json
class FetchNodeValue:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"node_name": ("STRING", {"multiline": False}),
"widget_name": ("STRING", {"multiline": False}),
"multiple": (["yes", "no"], {"default": "no"}),
},
"hidden": {"extra_pnginfo": "EXTRA_PNGINFO",
"prompt": "PROMPT"},
}
RETURN_TYPES = ("STRING", )
FUNCTION = "get_widget_value"
CATEGORY = "utils/hus"
def get_widget_value(self, node_name, widget_name, multiple, extra_pnginfo, prompt):
workflow = extra_pnginfo["workflow"]
results = []
multiple = multiple == "yes"
for node in workflow["nodes"]:
node_id = None
name = node["type"]
if "properties" in node:
if "Node name for S&R" in node["properties"]:
name = node["properties"]["Node name for S&R"]
if name == node_name:
node_id = node["id"]
else:
if "title" in node:
name = node["title"]
if name == node_name:
node_id = node["id"]
if node_id is None:
continue
values = prompt[str(node_id)]
if "inputs" in values:
if widget_name in values["inputs"]:
v = values["inputs"][widget_name]
if not multiple:
return (v, )
results.append(v)
else:
raise NameError(f"Widget not found: {node_name}.{widget_name}")
if not results:
raise NameError(f"Node not found: {node_name}.{widget_name}")
return (", ".join(results).strip(", "), )
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
class BatchState:
def __init__(self):
self.state_hash = False
self.count = 0
@staticmethod
def collect_state(extra_pnginfo, prompt):
workflow = extra_pnginfo["workflow"]
results = {}
if "links" in workflow:
results["__links"] = workflow["links"]
for node in workflow["nodes"]:
node_id = str(node["id"])
name = node["type"]
if "Debug" in name or "Show" in name or "Function" in name or "Evaluate" in name:
continue
if "widgets_values" in node and "inputs" not in node:
results[node_id] = node["widgets_values"]
elif node_id in prompt:
values = prompt[node_id]
if "inputs" in values:
results[node_id] = {}
for widget in values["inputs"].items():
(n, v) = widget
if type(v) is not str and isinstance(v, collections.abc.Sequence):
continue
results[node_id][n] = v
elif "widgets_values" in node:
results[node_id] = node["widgets_values"]
result = json.dumps(results, sort_keys=True)
return hashlib.sha256(result.encode()).hexdigest()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
},
"hidden": {"extra_pnginfo": "EXTRA_PNGINFO",
"prompt": "PROMPT"},
}
RETURN_TYPES = ("NUMBER", "STRING", "NUMBER", "INT", )
RETURN_NAMES = ("changed", "hash", "count (number)", "count (int)", )
FUNCTION = "check_state"
CATEGORY = "utils/hus"
def check_state(self, extra_pnginfo, prompt):
old = self.state_hash
self.state_hash = self.collect_state(extra_pnginfo, prompt)
if self.state_hash == old:
self.count = 0
return (0, self.state_hash, self.count, self.count, )
self.count += 1
return (1, self.state_hash, self.count, self.count, )
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
+68
View File
@@ -0,0 +1,68 @@
import os
import re
import folder_paths
class PromptStylerCSV3Way:
"""
Adapted from "Load Styles CSV" (https://github.com/theUpsider/ComfyUI-Styles_CSV_Loader).
Split into G and L prompt for SDXL and changed to passthrough for chaining
"""
@staticmethod
def load_styles_csv(styles_path: str):
"""Loads csv file with styles. It has only one column.
Ignore the first row (header).
positive_prompt are strings separated by comma. Each string is a prompt.
negative_prompt are strings separated by comma. Each string is a prompt.
Returns:
list: List of styles. Each style is a dict with keys: style_name and value: [positive_prompt, negative_prompt]
"""
styles = {"Error loading styles.csv, check the console": ["",""]}
if not os.path.exists(styles_path):
print(f"""Error. No styles.csv found. Put your styles.csv in {styles_path}. Then press "Refresh".
""")
return styles
try:
with open(styles_path, "r", encoding="utf-8") as f:
styles = [[x.replace('"', '').replace('\n','') for x in re.split(',(?=(?:[^"]*"[^"]*")*[^"]*$)', line)] for line in f.readlines()[1:]]
styles = {x[0]: [x[1],x[2],x[3]] for x in styles}
except Exception as e:
print(f"""Error loading styles.csv. Make sure it is in {styles_path}. Then press "Refresh".
Error: {e}
""")
return styles
@classmethod
def INPUT_TYPES(cls):
cls.styles_folder = os.path.dirname(os.path.realpath(__file__))
cls.styles_csv = cls.load_styles_csv(os.path.join(cls.styles_folder, "styles.csv"))
return {
"required": {
"styles": (list(cls.styles_csv.keys()),),
"positive_g": ("STRING", {"multiline": True, "forceInput": True}),
"positive_l": ("STRING", {"multiline": True, "forceInput": True}),
"negative": ("STRING", {"multiline": True, "forceInput": True}),
},
}
RETURN_TYPES = ("STRING","STRING","STRING")
RETURN_NAMES = ("positive prompt (g)", "supporting terms (l)", "negative prompt")
FUNCTION = "execute"
CATEGORY = "utils/hus"
def execute(self, styles, positive_g, positive_l, negative):
if styles == "none":
return (positive_g, positive_l, negative)
pg = self.styles_csv[styles][0]
if "\u007bprompt\u007d" in pg:
pg = pg.replace("\u007bprompt\u007d", positive_g)
else:
pg += " " + positive_g
pl = positive_l + ", " + self.styles_csv[styles][1]
n = negative + ", " + self.styles_csv[styles][2]
return (pg.strip(" ,"), pl.strip(" ,"), n.strip(" ,"))
+48
View File
@@ -0,0 +1,48 @@
import os
import re
import folder_paths
import hashlib
import datetime
import pprint
import collections
import json
class TextHash:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"multiline": True, "forceInput": True}),
"length": ("INT", {"default": 8, "min": 1, "max": 40, "step": 1}),
},
}
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("hash",)
FUNCTION = "hash_text"
CATEGORY = "utils/hus"
def hash_text(self, text, length):
return (hashlib.sha256(text.encode()).hexdigest()[:length],)
class DateTimeFormat:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"format": ("STRING", {"multiline": False}),
},
}
RETURN_TYPES = ("STRING", )
FUNCTION = "datetime_fmt"
CATEGORY = "utils/hus"
def datetime_fmt(self, format):
return (datetime.datetime.now().strftime(format), )
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")