initial commit
This commit is contained in:
@@ -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
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
|
@@ -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")
|
||||
@@ -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(" ,"))
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user