Initial commit 1.0.0
@@ -1,2 +1,58 @@
|
||||
# cg-prompt-info
|
||||
# Prompt Info
|
||||
|
||||
A set of three nodes to help retrieve information from previously generated images.
|
||||
|
||||
To install:
|
||||
```
|
||||
cd [comfy]/custom_nodes
|
||||
git clone https://github.com/chrisgoringe/cg-prompt-info
|
||||
```
|
||||
|
||||
Leave a star if you like these nodes - and [add an issue](https://github.com/chrisgoringe/cg-prompt-info/issues) if you have problems or ideas.
|
||||
|
||||
## Basic idea
|
||||
|
||||
Everyone knows that the workflow gets saved into an image saved by ComfyUI so you can just drop the image into a ComfyUI window to recreate the workflow that produced it. That's cool. Great feature.
|
||||
|
||||
But sometimes I don't want the whole workflow - I just want to know what prompt I used, or what checkpoint, or LoRA, or whatever.
|
||||
|
||||
That's where the first two nodes come in. Load an image using the `Load Image With Info` node, and you get an extra output, called `info`. Plug that into the `Extract Info`, and run:
|
||||
|
||||
|You get a list of nodes from the original workflow with information available|Pick one, and you get a list of input and output* values|Pick one, and see what it was|
|
||||
|-|-|-|
|
||||
||||
|
||||
|
||||
The outputs of the `Extract Info` node contain the displayed value as a string, and, if it can be parsed, as a float and an int - so you can use the values you've extracted.
|
||||
|
||||
## *Adding more info
|
||||
|
||||
You'll quickly notice that only input values are available, and then only if they were widgets (not inputs). That's all that's stored in the prompt - it's all Comfy needed to do the run. But what if the prompt depended on external files that might have changed, or used random numbers? Or had some really complicated flow so it's hard to get the information you want?
|
||||
|
||||
That's the point of the third node. It adds extra data when you save. Get into the habit of using it, you can thank me later.
|
||||
|
||||
|Put it just before you save the image, and it adds all the output values of every node that has already been executed|So there's data on a lot more nodes|Including outputs|
|
||||
|-|-|-|
|
||||
||||
|
||||
|
||||
## What's stored?
|
||||
|
||||
Obviously lots of the node outputs contain big python objects representing checkpoints, images, latents etc.. They aren't stored, only STRING, INT or FLOAT values are (strictly, it's anything represented by a python `str`, `int`, or `float`, which might include some other stuff - who knows?). In other cases, what you get is the first 30 characters of the string representation, which is normally enough to tell you what the thing was...
|
||||
|
||||

|
||||
|
||||
## Update and uninstall
|
||||
|
||||
To update:
|
||||
```
|
||||
cd [comfy]/custom_nodes/cg-prompt-info
|
||||
git pull
|
||||
```
|
||||
|
||||
To remove, delete `[comfy]/custom_nodes/cg-prompt-info` and `[comfy]/web/extensions/cg_prompt_info`. Yeah, underscores in that last one.
|
||||
|
||||
# Shameless Plug
|
||||
|
||||
|Check out [Image Picker](https://github.com/chrisgoringe/cg-image-picker), a node that lets you pause the flow, choose an image from a batch, and send it into the rest of the flow|And [Use Everywhere](https://github.com/chrisgoringe/cg-use-everywhere) nodes to broadcast data invisibily, eliminating the need for lots of connection spaghetti|
|
||||
|-|-|
|
||||
|||
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import sys, os, re
|
||||
sys.path.insert(0,os.path.dirname(os.path.realpath(__file__)))
|
||||
from .prompt_info import *
|
||||
|
||||
NODE_CLASS_MAPPINGS= {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
VERSION = "1.0.0"
|
||||
|
||||
def pretty(name:str):
|
||||
return " ".join(re.findall("[A-Z][a-z]*", name))
|
||||
|
||||
for clazz in CLAZZES:
|
||||
name = clazz.__name__
|
||||
NODE_CLASS_MAPPINGS[name] = clazz
|
||||
NODE_DISPLAY_NAME_MAPPINGS[name] = pretty(name)
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
import shutil
|
||||
import folder_paths
|
||||
|
||||
application_root_directory = os.path.dirname(folder_paths.__file__)
|
||||
application_web_extensions_directory = os.path.join(application_root_directory, "web", "extensions", "cg_prompt_info")
|
||||
module_root_directory = os.path.dirname(os.path.realpath(__file__))
|
||||
module_js_directory = os.path.join(module_root_directory, "js")
|
||||
|
||||
shutil.copytree(module_js_directory, application_web_extensions_directory, dirs_exist_ok=True)
|
||||
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,34 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
import { ComfyWidgets } from "../../../scripts/widgets.js";
|
||||
|
||||
function displayMessage(message) {
|
||||
var w = this.widgets?.find((w) => w.name === "display_text_widget");
|
||||
if (w === undefined) {
|
||||
w = ComfyWidgets["STRING"](this, "display_text_widget", ["STRING", { multiline: true }], app).widget;
|
||||
w.inputEl.readOnly = true;
|
||||
w.inputEl.style.opacity = 0.6;
|
||||
w.inputEl.style.fontSize = "9pt";
|
||||
}
|
||||
w.value = message;
|
||||
this.onResize?.(this.size);
|
||||
};
|
||||
|
||||
app.registerExtension({
|
||||
name: "cg.info.textmessage",
|
||||
async setup() {
|
||||
function messageHandler(event) {
|
||||
const id = event.detail.id;
|
||||
const message = event.detail.message;
|
||||
const node = app.graph._nodes_by_id[id];
|
||||
if (node && node.displayMessage) node.displayMessage(message);
|
||||
else (console.log(`node ${id} couldn't handle a message`));
|
||||
}
|
||||
api.addEventListener("cg.prompt_info.textmessage", messageHandler);
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeType.comfyClass=="ExtractInfo" ) {
|
||||
nodeType.prototype.displayMessage = displayMessage;
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,142 @@
|
||||
import inspect, json
|
||||
from nodes import LoadImage
|
||||
from folder_paths import get_annotated_filepath
|
||||
from PIL import Image
|
||||
from server import PromptServer
|
||||
|
||||
def get_outputs():
|
||||
for frame_info in inspect.stack():
|
||||
executor_maybe = frame_info.frame.f_locals.get("e",None)
|
||||
if hasattr(executor_maybe,'outputs'):
|
||||
return executor_maybe.outputs
|
||||
|
||||
def represent(thing):
|
||||
if isinstance(thing,str) or isinstance(thing, int) or isinstance(thing, float):
|
||||
return thing
|
||||
return f"{thing}"[:30]
|
||||
|
||||
def insertAllInAndOut(prompt, extra, all_outputs=None):
|
||||
extra['workflow']['values'] = {}
|
||||
for node in extra['workflow']['nodes']:
|
||||
node_id = str(node['id'])
|
||||
out = {}
|
||||
inp = {}
|
||||
type = node['type']
|
||||
print(f"Node {node_id} ({type})")
|
||||
if all_outputs and 'outputs' in node:
|
||||
for i, output in enumerate(node['outputs']):
|
||||
try:
|
||||
out[output['name']] = represent(all_outputs[node_id][i][0])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if node_id in prompt and 'inputs' in prompt[node_id]:
|
||||
for name in prompt[node_id]['inputs']:
|
||||
v = prompt[node_id]['inputs'][name]
|
||||
if isinstance(v,list):
|
||||
if all_outputs and v[0] in all_outputs:
|
||||
v = all_outputs[v[0]][v[1]][0]
|
||||
else:
|
||||
continue
|
||||
inp[name] = represent(v)
|
||||
|
||||
if inp or out:
|
||||
extra['workflow']['values'][node_id] = {"type":node['type'],"inputs":inp,"outputs":out}
|
||||
|
||||
class AddInfo():
|
||||
CATEGORY = "prompt_info"
|
||||
FUNCTION = "func"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return { "required":{"image":("IMAGE",{})}, "hidden":{"prompt":"PROMPT", "extra":"EXTRA_PNGINFO"} }
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
|
||||
def func(self, image, prompt, extra):
|
||||
insertAllInAndOut(prompt, extra, get_outputs())
|
||||
return (image,)
|
||||
|
||||
class LoadImageWithInfo(LoadImage):
|
||||
CATEGORY = "prompt_info"
|
||||
FUNCTION = "func"
|
||||
|
||||
RETURN_TYPES = LoadImage.RETURN_TYPES + ("STRING",)
|
||||
RETURN_NAMES = ("image", "mask", "info")
|
||||
|
||||
def func(self, image):
|
||||
loaded = self.load_image(image)
|
||||
filepath = get_annotated_filepath(image)
|
||||
with Image.open(filepath) as img:
|
||||
extra_pnginfo_loaded = img.text if hasattr(img,'text') else {}
|
||||
|
||||
if not 'workflow' in extra_pnginfo_loaded: # image doesn't have workflow saved
|
||||
return loaded + (json.dumps({}),)
|
||||
|
||||
extra_pnginfo_loaded['workflow'] = json.loads(extra_pnginfo_loaded['workflow'])
|
||||
prompt = json.loads(extra_pnginfo_loaded['prompt'])
|
||||
|
||||
if not 'values' in extra_pnginfo_loaded['workflow']: # workflow doesn't have values - do what we can
|
||||
insertAllInAndOut(prompt, extra_pnginfo_loaded)
|
||||
|
||||
return loaded + (json.dumps(extra_pnginfo_loaded['workflow']['values'], indent=2),)
|
||||
|
||||
class ExtractInfo():
|
||||
CATEGORY = "prompt_info"
|
||||
FUNCTION = "func"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"info": ("STRING", {"forceInput": True}),
|
||||
"node_id": ("INT", {"default": 0 }),
|
||||
"side": (["inputs","outputs"],{}),
|
||||
"name": ("STRING", {"default":""},)
|
||||
}, "hidden": {"id":"UNIQUE_ID"}}
|
||||
|
||||
RETURN_TYPES = ("STRING","FLOAT","INT")
|
||||
RETURN_NAMES = ("string","float","int")
|
||||
def func(self, info, node_id, side, name, id):
|
||||
try:
|
||||
thing = json.loads(info)
|
||||
except json.JSONDecodeError:
|
||||
print("JSONDecodeError")
|
||||
return (info,0,0)
|
||||
|
||||
try:
|
||||
thing = thing[str(node_id)]
|
||||
except KeyError:
|
||||
text = "\n".join(["Available Nodes:",]+[f"{id} ({thing[id]['type']})" for id in thing])
|
||||
PromptServer.instance.send_sync("cg.prompt_info.textmessage", {"id": id, "message":text})
|
||||
return (thing, None, None)
|
||||
|
||||
try:
|
||||
thing = thing[side][name]
|
||||
except KeyError:
|
||||
text = f"Available values on {node_id}:\n\n"
|
||||
if len(thing['inputs']):
|
||||
text += "\n".join(["inputs:",]+[f"{name}" for name in thing['inputs']]+["\n"])
|
||||
if len(thing['outputs']):
|
||||
text += "\n".join(["outputs:",]+[f"{name}" for name in thing['outputs']]+[""])
|
||||
PromptServer.instance.send_sync("cg.prompt_info.textmessage", {"id": id, "message":text})
|
||||
return (thing, None, None)
|
||||
|
||||
if not isinstance(thing,str):
|
||||
thing = json.dumps(thing, indent=2)
|
||||
|
||||
PromptServer.instance.send_sync("cg.prompt_info.textmessage", {"id": id, "message":thing})
|
||||
|
||||
try:
|
||||
flt = float(thing)
|
||||
except ValueError:
|
||||
return (thing, None, None)
|
||||
|
||||
try:
|
||||
nt = int(thing)
|
||||
except ValueError:
|
||||
return (thing, flt, None)
|
||||
|
||||
return (thing, flt, nt)
|
||||
|
||||
CLAZZES = [AddInfo, LoadImageWithInfo, ExtractInfo]
|
||||