Initial commit 1.0.0

This commit is contained in:
Chris
2023-10-19 16:24:48 +11:00
parent d21c62018f
commit 0134542743
11 changed files with 262 additions and 2 deletions
+58 -2
View File
@@ -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|
|-|-|-|
|![1](docs/1.png)|![2](docs/2.png)|![3](docs/3.png)|
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|
|-|-|-|
|![4](docs/4.png)|![5](docs/5.png)|![6](docs/6.png)|
## 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...
![](docs/7.png)
## 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|
|-|-|
|![ip](https://raw.githubusercontent.com/chrisgoringe/cg-image-picker/main/docs/Screenshot%20both.png)|![ue](https://github.com/chrisgoringe/cg-use-everywhere/raw/main/docs/test-workflow-screenshot.png)|
+28
View File
@@ -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)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+34
View File
@@ -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;
}
}
})
+142
View File
@@ -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]