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
安装命令
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.
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

2026年9月24日

分类

未分类

许可证

MIT

源路径

comfyui-plugin/skills/comfy-metadata

默认分支

main

最新提交

1668324

Tree SHA

b2d4cc3