Add files via upload

This commit is contained in:
TakkunRed
2026-04-04 23:18:58 +09:00
committed by GitHub
parent 8771eafff1
commit 384b952e82
4 changed files with 311 additions and 2 deletions
+147
View File
@@ -0,0 +1,147 @@
import json
import os
import datetime
import re
from PIL import Image, ExifTags
import numpy as np
from nodes import LoadImage
from folder_paths import get_annotated_filepath
class LoadImageWithFileName(LoadImage):
@classmethod
def INPUT_TYPES(s):
return super().INPUT_TYPES()
RETURN_TYPES = ("IMAGE", "MASK", "STRING")
RETURN_NAMES = ("image", "mask", "image_name")
FUNCTION = "load_image_info"
CATEGORY = "image"
def load_image_info(self, image):
img, mask = super().load_image(image)
return (img, mask, image)
class ExtractPngMetadata:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image_name": ("STRING", {"default": "example.png"}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("metadata_json",)
FUNCTION = "extract"
CATEGORY = "image/metadata"
def extract(self, image_name):
if os.path.isabs(image_name):
image_path = image_name
else:
image_path = get_annotated_filepath(image_name)
all_metadata = {}
def json_serializable(obj):
try:
if isinstance(obj, (datetime.date, datetime.datetime)):
return obj.isoformat()
if isinstance(obj, bytes):
try:
s = obj.decode('utf-8', errors="replace").strip('\x00')
except:
s = obj.decode('cp932', errors="replace").strip('\x00')
return re.sub(r'^(ASCII|UNICODE|JIS|UNDEFINED)\x00*', '', s).strip()
return str(obj)
except:
return f"<Unserializable>"
def try_parse_json(value):
val_str = value if isinstance(value, str) else json_serializable(value)
stripped = val_str.strip()
json_match = re.search(r'(\{.*\}|\[.*\])', stripped, re.DOTALL)
if json_match:
try:
parsed = json.loads(json_match.group(0))
if isinstance(parsed, str):
return try_parse_json(parsed)
return parsed
except:
pass
if "Steps:" in stripped and "Seed:" in stripped:
return [line.strip() for line in stripped.split('\n') if line.strip()]
return val_str
if os.path.exists(image_path):
stat = os.stat(image_path)
all_metadata["fileinfo"] = {
"filename": os.path.basename(image_path),
"date": datetime.datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
"size": f"{stat.st_size / 1024:.2f} KB",
}
with Image.open(image_path) as img:
all_metadata["fileinfo"]["resolution"] = f"{img.width}x{img.height}"
for key, value in img.info.items():
if key == "exif": continue
all_metadata[key] = try_parse_json(value)
try:
exif_obj = img.getexif()
if exif_obj:
exif_results = {}
for tag, value in exif_obj.items():
tag_name = ExifTags.TAGS.get(tag, f"Tag_{tag}")
exif_results[f"{tag_name} (ID: {tag})"] = try_parse_json(value)
exif_ifd = exif_obj.get_ifd(0x8769)
if exif_ifd:
for tag, value in exif_ifd.items():
tag_name = ExifTags.TAGS.get(tag, f"ExifIFD_Tag_{tag}")
exif_results[f"{tag_name} (ID: {tag})"] = try_parse_json(value)
all_metadata["EXIF_Full_Data"] = exif_results
except Exception as e:
all_metadata["EXIF_Error"] = f"EXIF Deep Scan failed: {str(e)}"
else:
all_metadata = {"error": "File not found"}
return (json.dumps(all_metadata, default=json_serializable),)
class JsonVisualizer:
@classmethod
def INPUT_TYPES(s):
return {"required": {"json_string": ("STRING", {"forceInput": True})}}
RETURN_TYPES = ("STRING",)
FUNCTION = "visualize"
CATEGORY = "ui/visualizer"
OUTPUT_NODE = True
def visualize(self, json_string):
import json
try:
data = json.loads(json_string)
except:
data = {"error": "Invalid JSON"}
return {"ui": {"json_data": [data]}, "result": (json_string,)}
NODE_CLASS_MAPPINGS = {
"LoadImageWithFileName": LoadImageWithFileName,
"ExtractPngMetadata": ExtractPngMetadata,
"JsonVisualizer": JsonVisualizer
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LoadImageWithFileName": "Load Image w/ Name",
"ExtractPngMetadata": "Extract PNG Metadata",
"JsonVisualizer": "JSON Tree Viewer"
}
+51 -2
View File
@@ -1,2 +1,51 @@
# ComfyUI-Metadata-Inspector
ComfyUI-Metadata-Inspector
# ComfyUI-Metadata-Inspector
A specialized suite of custom nodes for ComfyUI designed to hunt down and visualize generation parameters (prompts, steps, samplers, etc.) embedded in AI-generated images. It works across different formats and platforms (ComfyUI, Automatic1111, Civitai, and more).
[![GitHub license](https://img.shields.io/github/license/TakkunRed/ComfyUI-Metadata-Inspector)](https://github.com/TakkunRed/ComfyUI-Metadata-Inspector/blob/main/LICENSE)
[![ComfyUI](https://img.shields.io/badge/ComfyUI-Custom_Nodes-blue)](https://github.com/comfyanonymous/ComfyUI)
## Core Mission
The primary goal of this tool is to **reveal hidden prompt strings and workflow structures** from AI-generated content. Whether it's a PNG with a complex node graph or a JPEG with a text-based prompt hidden in EXIF data, this inspector brings it to light.
## Node Descriptions
This extension includes three essential nodes to complete the inspection workflow:
### 1. Load Image w/ Name
- **Purpose**: Provides a flexible way to load images.
- **Function**: Unlike the standard loader, it allows you to input a filename via string or absolute path, making it easier to integrate with dynamic file-handling workflows.
### 2. Extract Metadata (The Core)
- **Purpose**: The "brain" that scans the image file for generation data.
- **Function**:
- **PNG Deep Scan**: Extracts full ComfyUI/A1111 prompt and workflow JSONs.
- **JPEG/EXIF Recovery**: Deep-scans EXIF sub-IFDs to find hidden `UserComment` and `ImageDescription` tags often used by Civitai and Tensor.art.
- **Smart JSON Recovery**: Detects JSON structures even when they are buried inside plain-text metadata.
### 3. JSON Tree Viewer (Web UI)
- **Purpose**: Provides a human-readable visualization of the raw data.
- **Function**: Transforms the raw metadata string into an interactive, foldable tree view directly on the ComfyUI canvas. It highlights potential prompt areas in orange for quick identification.
## Key Features
- **Hidden Prompt Extraction**: Recovers data from JPEGs where standard tools fail.
- **Workflow Reconstruction Support**: Outputs raw JSON strings that can be used to understand complex node setups.
- **Multi-Platform Compatibility**: Supports metadata formats from ComfyUI, Automatic1111, and various web-based generators.
## Installation
### Manual Install
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/TakkunRed/ComfyUI-Metadata-Inspector.git
```
## How to Use
1. Place Load Image w/ Name.
2. Connect it to Extract Metadata.
3. Connect the metadata_json output to JSON Tree Viewer.
4. Input your image path and run the queue to see the hidden prompt data.
## License
MIT
+6
View File
@@ -0,0 +1,6 @@
from .Metadata_Inspector import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
# ここが重要です。文字列でディレクトリ名を指定します。
WEB_DIRECTORY = "web"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
+107
View File
@@ -0,0 +1,107 @@
import { app } from "../../../scripts/app.js";
app.registerExtension({
name: "Comfy.JsonTreeVisualizer", // 名前が他と被らないように変更
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// 対象を新しいノード名に変更
if (nodeData.name === "JsonVisualizer") {
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
// --- ここから成功していた時のロジックを完全再現 ---
const data = Array.isArray(message.json_data) ? message.json_data[0] : message.json_data;
if (!data) return;
if (this.widgets) {
const existing = this.widgets.findIndex(w => w.name === "MetadataViewer");
if (existing !== -1) {
this.widgets[existing].onRemove?.();
this.widgets.splice(existing, 1);
}
}
const hasPromptDeep = (obj) => {
for (const k in obj) {
const v = obj[k];
if (typeof v === "string" && v.length > 50) return true;
if (typeof v === "object" && v !== null && hasPromptDeep(v)) return true;
}
return false;
};
const createTree = (obj) => {
let html = '<div style="font-family: monospace; font-size: 11px; text-align: left;">';
for (const key in obj) {
const val = obj[key];
const isObject = typeof val === "object" && val !== null;
if (isObject) {
const classType = val.class_type ? ` <span style="color: #888; font-weight: normal; font-size: 10px;">(${val.class_type})</span>` : "";
const containsPrompt = hasPromptDeep(val);
const labelStyle = containsPrompt
? "color: #ffcc00; text-decoration: underline; background: rgba(255, 204, 0, 0.1);"
: "color: #f8b195;";
html += `
<div style="margin-bottom: 2px;">
<div style="cursor: pointer; ${labelStyle} display: flex; align-items: center; user-select: none;"
onclick="const c = this.nextElementSibling; c.style.display = c.style.display === 'none' ? 'block' : 'none'; this.querySelector('.arrow').innerText = c.style.display === 'none' ? '▶' : '▼'; event.stopPropagation();">
<span class="arrow" style="width: 12px; display: inline-block; color: #ffb347;">▶</span>
<strong>${key}${classType}</strong>
${containsPrompt ? ' <span style="font-size: 9px; margin-left: 5px; color: #ffcc00;">[★Prompt]</span>' : ''}
</div>
<div class="tree-content" style="display: none; margin-left: 10px; border-left: 1px solid #444; padding-left: 6px;">
${createTree(val)}
</div>
</div>`;
} else {
const isLongText = typeof val === "string" && val.length > 50;
const highlightStyle = isLongText
? "background-color: #1e2a3a; color: #ffeb99; padding: 2px 4px; border-radius: 2px; border: 1px solid #3d4d5d;"
: "color: #fff;";
const escapedVal = String(val).replace(/'/g, "\\'").replace(/"/g, '&quot;');
html += `
<div style="padding-left: 15px; color: #ddd; margin-bottom: 3px; display: flex;">
<span style="color: #99ffcc; white-space: nowrap; margin-right: 5px;">${key}:</span>
<span class="copyable"
style="${highlightStyle} cursor: copy; word-break: break-all;"
onclick="navigator.clipboard.writeText('${escapedVal}'); alert('Copied!'); event.stopPropagation();">
${val}
</span>
</div>`;
}
}
html += '</div>';
return html;
};
const viewerContainer = document.createElement("div");
viewerContainer.style.cssText = `
display: flex; flex-direction: column; width: 100%; height: 100%;
min-height: 250px; background: #0f0f0f; padding: 10px; margin-top: 5px;
border: 1px solid #333; border-radius: 4px; color: #eee; box-sizing: border-box;
`;
const header = document.createElement("div");
header.innerHTML = `PNG Info Viewer <span style="font-size: 9px; color: #666;">(★ = Potential Prompt)</span>`;
header.style.cssText = "border-bottom: 1px solid #333; margin-bottom: 8px; padding-bottom: 4px; color: #ffb347; font-weight: bold; flex-shrink: 0;";
viewerContainer.appendChild(header);
const treeRoot = document.createElement("div");
treeRoot.style.cssText = "overflow-y: auto; flex-grow: 1;";
treeRoot.innerHTML = createTree(data);
viewerContainer.appendChild(treeRoot);
const w = this.addDOMWidget("MetadataViewer", "view", viewerContainer);
w.serialize = false;
this.onResize = function(size) {
viewerContainer.style.height = (size[1] - 40) + "px";
};
if (this.size[1] < 200) this.setSize([Math.max(this.size[0], 500), 600]);
};
}
},
});