comfy-metadata

v2026.09.24

Extract/analyze ComfyUI metadata embedded in output files - PNG tEXt, WebP EXIF, MP4/WebM container, .latent safetensors. Use when figuring out what prompt/settings produced a file, or comparing runs.

GitHub
Install command
npx skhub add laurigates/comfy-metadata
Markdown
SKILL.md

ComfyUI output metadata

Every image/video/audio file ComfyUI emits embeds the API-form prompt and the UI-form workflow that produced it. The encoding varies by file format and by which save-node wrote it (core vs. kijai vs. VHS). This skill is the canonical reference for where the data lives, how to read it back, and a small Python toolkit to do it reliably across every format on disk in this install.

When to Use This Skill

Use this skill when...Use instead when...
Figuring out what prompt/settings produced an existing output fileInspecting a live in-graph value during a run -> comfy-debug-preview
Scanning, organizing, or comparing a directory of outputsAuto-arranging the layout of a workflow JSON -> comfy-workflow-layout

Quick reference

FormatSaved byWhere the JSON livesEncoding
PNG stillcore SaveImage, kijai PNG pathtEXt chunks (PIL Image.info["prompt"] and ["workflow"])each value is a JSON string
Animated PNGcore SaveAnimatedPNGiTXt chunks (same keys)each value is a JSON string
WebP still + animatedcore SaveAnimatedWEBPEXIF tags: 0x0110 (Model) holds "prompt:<json>", 0x010F (Make) holds "workflow:<json>", lower tags hold further extra_pnginfo keysone EXIF string per key, "key:json" prefix
MP4 nativecore SaveVideoContainer metadata, separate keys: prompt, workflow, plus any extraeach value is a JSON string
MP4 kijai (WanVideoWrapper_*.mp4)WanVideoWrapper.save_videoContainer metadata, single key commentone JSON object: {"prompt": "<json>", "workflow": "<json>"} (double-encoded)
WebM / Matroskacore SaveVideo; kijai/MMAudioContainer metadata: per-key (native) or single COMMENT (kijai)same patterns as MP4
FLAC / OGG / MP3 / WAVcore SaveAudioContainer metadata: prompt, extra_pnginfo* keyseach value is a JSON string
.latentcore SaveLatentsafetensors header metadata: prompt, workfloweach value is a JSON string

The library handles all of these uniformly. See REFERENCE.md for code anchors, exact byte-level details, and edge cases (the _create_webp_metadata EXIF tag walk, extra_pnginfo keys beyond workflow, the kijai double-encoded comment format, fp8-scaled safetensors metadata, etc.).

Toolkit

scripts/comfy_meta.py is a single self-contained Python file. It works as both a library and a CLI with four subcommands. It uses PIL (for PNG/WebP), PyAV (for MP4/WebM/audio), and safetensors (for .latent) — all already installed in .venv/.

Run via the project venv:

.venv/bin/python .claude/skills/comfy-metadata/scripts/comfy_meta.py <subcommand> ...

Library use (batch scripts)

For ad-hoc batch work — renaming, indexing, clustering — calling the CLI once per file is slow. Import comfy_meta directly instead. It has no package wrapper, so add its dir to sys.path first:

import sys, pathlib
sys.path.insert(0, str(pathlib.Path(".claude/skills/comfy-metadata/scripts")))
import comfy_meta

for p in pathlib.Path("output").iterdir():
    if not p.is_file():
        continue
    ex = comfy_meta.extract(p)        # {"prompt": <api-dict>, "workflow": <ui-dict>}
    prompt = ex.get("prompt")
    if not isinstance(prompt, dict) or not prompt:
        continue                       # no embedded metadata
    summary = comfy_meta.summarize(prompt)
    print(p.name, summary.sampler, summary.scheduler, summary.seed)

extract() returns parsed JSON for both halves; summarize() walks the API prompt and yields a Summary dataclass. See scripts/rename_outputs.py for a full example that builds new filenames from summary.samplers[0] and the source file's mtime.

The UI workflow half is useful too

summarize() covers the API prompt, but extract()["workflow"] (the UI form) carries data the summarizer doesn't surface — most usefully save-node widgets. A workflow that wrote itself to a dedicated output bucket (<bucket>/<date>/…) self-labels its outputs, so the prefix is a free classification signal:

BUCKET = "<bucket>/"          # whatever prefix your install sorts into

ex = comfy_meta.extract(p)
workflow = ex.get("workflow") or {}
for n in workflow.get("nodes", []) or []:
    wv = n.get("widgets_values")
    # SaveImage / SaveWEBM: list[0] is the filename_prefix
    if isinstance(wv, list) and wv and isinstance(wv[0], str) and wv[0].startswith(BUCKET):
        return BUCKET.rstrip("/")
    # VHS_VideoCombine: dict["filename_prefix"]
    if isinstance(wv, dict) and str(wv.get("filename_prefix", "")).startswith(BUCKET):
        return BUCKET.rstrip("/")

rename_outputs.py's NSFW classifier combines this self-label signal with API-prompt asset-name token matching (model / text-encoder / LoRA names). The same approach works for any other categorisation the UI workflow encodes that the API prompt strips out: node titles, custom properties, group names, etc.

extract — dump the embedded JSON

# Both prompt + workflow as one JSON object on stdout
.venv/bin/python .../comfy_meta.py extract output/WanVideoWrapper_I2V_00001.png

# Just one half (suitable for piping to jq)
.venv/bin/python .../comfy_meta.py extract -k prompt   path/to.png | jq .
.venv/bin/python .../comfy_meta.py extract -k workflow path/to.mp4  | jq '.nodes | length'

# Re-import a downloaded JPEG/MP4 back into ComfyUI by saving its workflow:
.venv/bin/python .../comfy_meta.py extract -k workflow some.mp4 > user/default/workflows/2026-05/recovered.json

summary — one-line, analysis-friendly settings

The summarizer walks the API-form prompt and pulls out the fields that actually matter for "what was different between run A and run B": model, text encoders, VAE, every sampler invocation (sampler/scheduler/steps/ cfg/denoise/seed), latent dims, num_frames, every LoRA + strength, and the positive/negative prompt text.

.venv/bin/python .../comfy_meta.py summary output/WanVideoWrapper_I2V_00001.mp4

Output is JSON; use -p for a human-readable two-column print instead.

scan — index a directory into JSONL

Walk a tree (recursively by default) and emit one JSON record per output file. Use this to build a queryable index of every render on disk.

.venv/bin/python .../comfy_meta.py scan output/ -o /tmp/runs.jsonl

# Then analyze with jq:
jq -r '[.path, .summary.steps, .summary.cfg, .summary.sampler] | @tsv' /tmp/runs.jsonl

# Group by sampler+steps+cfg to see what combinations were used:
jq -s 'group_by(.summary.sampler+"|"+(.summary.steps|tostring)+"|"+(.summary.cfg|tostring))
       | map({key: .[0].summary | "\(.sampler) steps=\(.steps) cfg=\(.cfg)", count: length})' \
  /tmp/runs.jsonl

Files without embedded metadata (e.g. phone photos in the same tree) get {"path": "...", "error": "no metadata"} so the index still covers everything.

diff — what changed between two runs

.venv/bin/python .../comfy_meta.py diff a.mp4 b.mp4

Prints a unified diff of the summarized settings. Useful when one of two near-identical workflows produced a better result and you want to see which knob actually moved.

What the summary captures

model:        UNETLoader.unet_name / CheckpointLoaderSimple.ckpt_name
              / WanVideoModelLoader.model / Image-Edit's diffusion path
text_encoders [list]: CLIPLoader / DualCLIPLoader / TripleCLIPLoader
              / LoadWanVideoT5TextEncoder / TextEncoderLoaderHiDream …
vae:          VAELoader.vae_name / WanVideoVAELoader.model_name
samplers [list]: every KSampler / KSamplerAdvanced / WanVideoSampler /
              WanVideoSamplerv2 / SamplerCustomAdvanced — each with
              {sampler, scheduler, steps, cfg, denoise, seed, start_step,
               end_step, add_noise} as found
latent_dims:  width × height from EmptyLatentImage / EmptySD3LatentImage /
              EmptyMochiLatentVideo / WanVideoEmptyEmbeds / etc.
num_frames:   from WanVideoEmptyEmbeds.num_frames / Empty*Video.length
loras [list]: every LoraLoader / LoraLoaderModelOnly / Power Lora Loader
              entry — {name, model_strength, clip_strength}
shift:        ModelSamplingAuraFlow / ModelSamplingSD3 shift values
positive [list], negative [list]: CLIPTextEncode-style text inputs, with
              the upstream node's title as a hint when present

Heuristic, not exhaustive — but covers ~95% of the workflows on this install. New node-types missing from the summarizer are still preserved in the raw prompt half of extract; add them to comfy_meta.py's SUMMARIZERS registry when a class becomes worth pulling out.

When the toolkit returns "no metadata"

A few cases that look like ComfyUI outputs but lack the JSON:

  • ComfyUI launched with --disable-metadata — the save nodes short-circuit before adding tEXt/EXIF/container tags.
  • Re-encoded with ffmpeg — ffmpeg -i in.mp4 -c copy out.mp4 does preserve container metadata; -c:v libx264 … (re-encode) typically drops it unless -map_metadata 0 is passed.
  • Re-saved through an image editor (Affinity, Photoshop, GIMP) — most strip tEXt chunks and rewrite EXIF.
  • Output from a frontend that bypasses save nodes (custom HTTP pipelines, Hugging Face Spaces wrapping ComfyUI, …).

For the second case, when you mv or cp files between dirs the metadata is fine — the OS-level operations preserve byte content. Only re-encoding strips it.

Privacy note

The embedded prompt JSON contains the full positive and negative text prompts, the exact seed, file paths to LoRAs/checkpoints/VAEs (which can leak local directory structure like models/loras/lgates/private_face_v1.safetensors), and sometimes authoring metadata in extra_pnginfo. Before sharing a ComfyUI output file publicly, decide whether you want to ship the metadata with it.

To strip metadata in-place (lossless):

  • PNG: oxipng --strip safe file.png (keeps colorspace, strips text)
  • WebP: re-encode with cwebp -metadata none or magick convert in.webp -strip out.webp
  • MP4/WebM: ffmpeg -i in.mp4 -c copy -map_metadata -1 out.mp4

Or set --disable-metadata on the ComfyUI server (in comfyui.service) if you want all future outputs to be metadata-free — but the toolkit becomes useless then.

Related skills

  • comfy-workflow-layout — once you've extracted a workflow with extract -k workflow, run it through scripts/layout_workflow.py to tidy node positions before importing.
  • comfy-cli — comfy node install-deps <workflow.json> consumes a workflow JSON file; pipe extract -k workflow straight into a temp file to install the missing custom nodes for an imported workflow.
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

comfyui-plugin/skills/comfy-metadata

Default branch

main

Latest commit

1668324

Tree SHA

b2d4cc3