102 lines
4.0 KiB
Python
102 lines
4.0 KiB
Python
import os
|
|
import json
|
|
from PIL import Image
|
|
import folder_paths
|
|
|
|
|
|
# Field separator for the flat wire format. Must be a char that never appears
|
|
# in node IDs, keys, or class names — subgraph node IDs contain ':' (e.g.
|
|
# "974:345"), so ':' can't be the delimiter. \x1f (ASCII unit separator) is safe.
|
|
SEP = "\x1f"
|
|
|
|
|
|
def _esc(val):
|
|
# Newlines are the wire-format line separator, so escape them inside values.
|
|
# Backslash first so the unescaping in JS is unambiguous.
|
|
return (
|
|
str(val)
|
|
.replace("\\", "\\\\")
|
|
.replace("\n", "\\n")
|
|
.replace("\r", "\\r")
|
|
)
|
|
|
|
|
|
class WorkflowInspectorNode:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
input_dir = folder_paths.get_input_directory()
|
|
files = [
|
|
f for f in os.listdir(input_dir)
|
|
if os.path.isfile(os.path.join(input_dir, f))
|
|
]
|
|
return {
|
|
"required": {
|
|
"image": (sorted(files), {"image_upload": True}),
|
|
},
|
|
"optional": {
|
|
"selected_leaf": ("STRING", {"default": ""}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING")
|
|
RETURN_NAMES = ("selected_text", "tree_summary")
|
|
FUNCTION = "inspect"
|
|
CATEGORY = "utils"
|
|
OUTPUT_NODE = True
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, image, selected_leaf=""):
|
|
image_path = folder_paths.get_annotated_filepath(image)
|
|
try:
|
|
return os.path.getmtime(image_path)
|
|
except OSError:
|
|
return float("nan")
|
|
|
|
def _result(self, selected_leaf, tree_summary):
|
|
# ui dict is what ComfyUI sends to the browser (picked up by node.onExecuted).
|
|
# result tuple is what flows to downstream nodes via RETURN_TYPES.
|
|
return {"ui": {"tree_summary": [tree_summary]}, "result": (selected_leaf, tree_summary)}
|
|
|
|
def inspect(self, image, selected_leaf=""):
|
|
image_path = folder_paths.get_annotated_filepath(image)
|
|
|
|
try:
|
|
img = Image.open(image_path)
|
|
prompt_str = img.info.get("prompt") or img.info.get("parameters")
|
|
if not prompt_str:
|
|
return self._result(selected_leaf, "ERROR:No ComfyUI metadata found in this image")
|
|
prompt = json.loads(prompt_str)
|
|
except json.JSONDecodeError as e:
|
|
return self._result(selected_leaf, f"ERROR:Invalid JSON in metadata: {e}")
|
|
except Exception as e:
|
|
return self._result(selected_leaf, f"ERROR:{e}")
|
|
|
|
lines = []
|
|
try:
|
|
# Numeric IDs sort first (by value), non-numeric keys after (by string).
|
|
# Returning a uniform (group, value) tuple avoids comparing int to str,
|
|
# which blows up when the metadata mixes numeric and non-numeric keys.
|
|
node_ids = sorted(
|
|
prompt.keys(),
|
|
key=lambda x: (0, int(x), "") if str(x).isdigit() else (1, 0, str(x)),
|
|
)
|
|
for node_id in node_ids:
|
|
node_data = prompt[node_id]
|
|
class_type = node_data.get("class_type", "Unknown")
|
|
# Newer ComfyUI stores the user-assigned title here; fall back to class.
|
|
title = node_data.get("_meta", {}).get("title", class_type)
|
|
lines.append(SEP.join(["NODE", node_id, class_type, _esc(title)]))
|
|
for key, val in node_data.get("inputs", {}).items():
|
|
# ComfyUI link reference: [node_id, output_index]
|
|
if isinstance(val, list) and len(val) == 2 and isinstance(val[1], int):
|
|
lines.append(SEP.join(["LINK", node_id, key, f"→{val[0]}"]))
|
|
elif isinstance(val, list):
|
|
lines.append(SEP.join(["LEAF", node_id, key, _esc(json.dumps(val))]))
|
|
else:
|
|
lines.append(SEP.join(["LEAF", node_id, key, _esc(val)]))
|
|
except Exception as e:
|
|
return self._result(selected_leaf, f"ERROR:Tree building failed: {e}")
|
|
|
|
return self._result(selected_leaf, "\n".join(lines))
|