""" comfyui_meta.py ================ Extraction and readable structuring of the ComfyUI metadata embedded in a PNG, MP3, FLAC or MP4/MOV file (workflow + prompt). This module has no Gradio dependency: it can be tested standalone from the command line: python3 comfyui_meta.py my_file.png General logic ------------- ComfyUI embeds two JSON blobs in the files it produces: - "prompt" : the "API" graph actually sent to the server for generation. Values here are already resolved (e.g. a dynamic prompt "{a|b}" already shows up picked as "a" or "b"). - "workflow" : the "UI" graph as displayed in ComfyUI (positions, titles, and most importantly "widgets_values" = what the user typed/set on each node, BEFORE resolution -> this is where the "{a|b|c}" template of a dynamic prompt node is found). We cross-reference the two to obtain, for each prompt actually used: - the final (resolved) value that was really sent to the engine - the original dynamic template, if a wildcard/dynamic prompt node was detected upstream. """ from __future__ import annotations import json import re import subprocess from dataclasses import dataclass, field from pathlib import Path from typing import Any # --------------------------------------------------------------------------- # 1. Raw extraction of the JSON blobs, depending on file type # --------------------------------------------------------------------------- class MetadataNotFound(Exception): pass def extract_raw_metadata(path: str | Path) -> dict[str, Any]: """Returns {'prompt': , 'workflow': } for a file. Raises MetadataNotFound if nothing could be extracted.""" path = Path(path) suffix = path.suffix.lower() if suffix in {".png", ".webp"}: data = _extract_from_image(path) elif suffix in {".mp3", ".flac", ".m4a", ".ogg"}: data = _extract_from_audio(path) elif suffix in {".mp4", ".mov", ".webm", ".mkv"}: data = _extract_from_video(path) else: # last resort: try image, then audio data = _extract_from_image(path) or _extract_from_audio(path) if not data or not (data.get("prompt") or data.get("workflow")): raise MetadataNotFound( f"No ComfyUI metadata (prompt/workflow) found in {path.name}." ) return data def _try_json(text: Any) -> Any: if not isinstance(text, str): return None text = text.strip() if not text: return None try: return json.loads(text) except (json.JSONDecodeError, TypeError): return None def _extract_from_image(path: Path) -> dict[str, Any] | None: try: from PIL import Image except ImportError: return None try: img = Image.open(path) except Exception: return None info = getattr(img, "info", {}) or {} result = {"prompt": _try_json(info.get("prompt")), "workflow": _try_json(info.get("workflow"))} if not result["prompt"] and not result["workflow"]: # PNG stores "prompt"/"workflow" as plain text chunks, which PIL # exposes directly through img.info (handled above). WEBP has no # such text-chunk mechanism, so ComfyUI instead embeds the same # JSON blobs inside EXIF tags, each value prefixed as # "