Tools for working with the metadata that AI image generators embed in their output.
For my stance on AI contributions, see the collaboration guidelines.
We use semantic versioning.
Everything is one command, chandra, with these subcommands:
| Command | What it does |
|---|---|
chandra show <png…> |
Read a ComfyUI image and print the AUTOMATIC1111/SD-Forge metadata that chandra inject would write. Read-only. |
chandra inject <png…> |
Write that metadata into the image(s), in place, so they're recognized by services and apps that don't analyze ComfyUI graphs — notably, CivitAI on upload, and SD Prompt Reader locally. |
chandra eject <png…> |
Remove that metadata again — the inverse of inject. Strips the parameters chunk and XMP description chandra wrote, leaving the original ComfyUI graph byte-intact. |
chandra search <terms…> |
Search the prompts embedded across a directory tree of generated images. |
chandra scrub <png…> |
Strip a ComfyUI image to an anonymized skeleton — graph wiring kept, image/prompts/docs removed — safe to share when reporting a parsing bug. Writes a copy; never modifies the original. |
Reading and writing are deliberately separate commands: show never modifies anything, and writing
only happens when you explicitly ask for inject (or eject, to undo it).
chandra show image.png # preview the synthesized metadata
chandra inject *.png # write metadata into a batch, in place
chandra inject imgs/ # …or hand it a directory (recursed)
chandra eject *.png # remove that metadata again (inverse of inject)
chandra search starfleet captain # find images whose prompt mentions a starfleet captain
chandra search catgirl -d imgs | chandra search -n blurry # chain searches to refine the result set
chandra search catgirl -d imgs | chandra inject # inject only the images a search foundEvery command takes the same inputs: files and/or directories (directories are recursed), or a
list of paths piped in on stdin, one per line — which is what lets a search feed show, inject,
or eject. search takes its roots with -d (its positional arguments are the search terms); the
others take them as positional arguments. With nothing to act on, each command prints a short usage
instead of guessing: bare chandra search asks for terms, bare chandra show / inject / eject
ask for paths. The one convenience is that search (once it has terms) defaults its search root to
the current directory; the writing commands never default to the cwd — so a bare chandra inject or
chandra eject can't modify files there by surprise.
Why this is useful: many services and apps such as CivitAI and SD Prompt Reader mostly punt on
analyzing ComfyUI workflows — a trivial txt2img graph is sometimes captured, but img2img, inpaint,
edit-mode, LoRA chains, and non-standard loaders are not. chandra walks the embedded ComfyUI graph
itself, reconstructs the recipe, and re-expresses it in the one format those tools read robustly.
chandra is interop-first — its whole point is sharing metadata — but it's built to widen your
exposure as little as possible while doing it:
- It runs entirely locally. No network calls, no telemetry, no phone-home — ever. Not even
--hash: AutoV2 hashes are computed from your local files, and CivitAI matches them on its side, only when you choose to upload. Nothing about your images leaves your machine onchandra's account. - Writing is opt-in and reversible.
shownever modifies anything;injectwrites only when you ask, and never defaults to the current directory;ejectremoves only the layer chandra stamped, leaving the original ComfyUI graph byte-for-byte intact. - It surfaces only the recipe you already embedded.
injectre-expresses the prompts, model, and settings ComfyUI wrote into the file — it adds nothing that wasn't already there. For images that aren't generations (background removers, pose detectors, …) it reports only the operations the graph performs, never user-controlled free text such as output filename patterns (which can carry usernames or paths). scrubfor safe sharing. To report a graph chandra misparses,scrubstrips a copy to an anonymized skeleton — graph wiring kept; image, prompt prose, and the editableworkflowchunk removed. The skeleton shows structure without being loadable back into ComfyUI, so sharing it can't reproduce your setup.
inject writes the recipe straight into the PNG, in place and losslessly — the original ComfyUI
prompt/workflow chunks are never touched. It writes two independent layers, both on by default: a
machine-readable A1111/SD-Forge parameters chunk (what CivitAI and SD Prompt Reader read) and an
XMP dc:description (what general image viewers show). The two sections below cover what each layer
is for — auto-linking your resources on CivitAI, and seeing the recipe in an everyday image viewer.
By default the checkpoint and LoRAs are named as plain text — readable by a human and by SD Prompt
Reader, but invisible to CivitAI, which keys its resource detection off hashes and surfaces nothing
without them. Add --hash (to show or inject) and chandra computes the AutoV2 hash
(sha256[:10]) of each file and emits Model hash: and Lora hashes:, which CivitAI matches to the
corresponding resource pages on upload:
chandra inject *.png --hash --models-dir ~/ComfyUI/modelsHashing needs the actual files, so you need to tell chandra where they live — either with
--models-dir DIR (repeatable) or via the CHANDRA_MODELS_DIR environment variable,
a PATH-style list of directories (colon-separated on Linux/macOS, semicolon on Windows):
export CHANDRA_MODELS_DIR=~/ComfyUI/models:~/extra/loras
chandra inject *.png --hash # picks up the dirs from the environmentOn Linux, to set the environment variable persistently, place the export command in your .bashrc.
The directories are indexed once and hashes are cached (keyed by path, size, and mtime), so a multi-GB checkpoint shared across a batch is hashed only the first time. Only the checkpoint and LoRAs auto-link on CivitAI — its detection covers nothing else.
The recipe also records the VAE (VAE:, plus VAE hash: under --hash) and any separate
text encoders — common on modern models (Flux, Qwen, …), often an LLM — as SD-Forge Module N
fields. CivitAI ignores both, but they're standard, faithful metadata that SD Prompt Reader, general
image viewers, and chandra show --recipe display; the text encoder in particular materially shapes
the result, so it's worth recording. Text encoders aren't hashed (no standard infotext hash field).
inject also embeds a clean, human-readable rendering of the recipe — the same information as
chandra show --recipe — as an XMP dc:description. So a general image viewer that reads standard
metadata (e.g. Pix, the Linux Mint viewer) shows the prompt and
settings in its Description caption, no SD software needed — often enough to skip opening a
dedicated prompt reader just to glance at what made an image. This is on by default; pass --no-xmp
to write only the machine-oriented parameters chunk. The two layers are independent and both
lossless — the original ComfyUI prompt/workflow chunks are never touched.
LoRAs differ between the layers, by design. The machine parameters chunk renders them in A1111's
inline <lora:name:strength> notation — that's the format's idiom, and the only standard place a
LoRA's strength is recorded.
ComfyUI itself never writes LoRAs into the prompt text, so the human-readable views keep the prose
clean and list them separately (LoRA: name (strength X) in the description and chandra show --recipe).
The inlined-into-prompt form is a data interchange convention.
Changed your mind? chandra eject is the inverse of inject: it removes the parameters chunk and
the XMP description, leaving the original ComfyUI prompt/workflow chunks byte-for-byte intact — an
inject followed by an eject restores the file exactly (byte-identical to the original, with the
same md5sum).
chandra eject *.png # remove chandra's metadata from a batch, in placeBy default eject removes only metadata chandra wrote — both layers carry a chandra-rosetta
stamp (the Version: field of the parameters chunk and the x:xmptk attribute of the XMP packet),
and anything unstamped is left alone, so it won't clobber a parameters block from A1111/Forge or an
XMP caption some other tool added. Two flags adjust that: --no-xmp removes only the parameters
chunk and leaves the XMP description; --force removes the parameters chunk and XMP regardless of
who wrote them.
chandra search builds boolean queries from three primitives — no special syntax or metacharacters:
| flag | example | |
|---|---|---|
| AND | (default) | chandra search cat photo — prompt contains both fragments |
| OR | --or (--any) |
chandra search --or captain admiral — either fragment |
| NOT | --not (--invert, -v) |
chandra search --not klingon — prompt lacks the fragment |
Fragments match as substrings, order-independent: cat photo also matches photocatalytic.
Fragments are smart-cased: an all-lowercase fragment is case-insensitive, a fragment with
any uppercase letter is case-sensitive. The flag -i forces case-insensitive.
chandra search is a *nix-style filter — matching paths go to stdout, and when input is piped,
it reads candidate paths from stdin. So chaining refines: each stage filters the previous
stage's results (set intersection), which gives full boolean in conjunctive normal form:
chandra search starship | chandra search --or captain admiral | chandra search --not klingon
# → starship AND (captain OR admiral) AND (NOT klingon)…and results compose with the rest of the shell:
chandra search wizard -d imgs | wc -l # count matches
chandra search cat -d imgs | xargs -d'\n' cp -t picks/ # copy matches elsewhere
chandra search catgirl -d imgs | fzf # pick one interactivelyMore flags:
-p/-nsearch the positive / negative prompt only,--exactmatches the whole query as one contiguous phrase instead of fragments,-C/--contextprints a highlighted snippet of each match, colorized on a terminal,--dirs-onlyprints matching directories instead of files, and-d DIRsets the search roots, repeatable; default is piped stdin, else the current directory.
chandra is Sanskrit for the moon (चन्द्र), the Hindu lunar deity. The metadata this tool
recovers is an image's nocturnal layer — dimmer than the bright pixels, easy to overlook, but there
to be read once you look for it. The name rewards a second glance: the astrophysicist Subrahmanyan
Chandrasekhar (of the Chandrasekhar limit)
carries the same root — Chandra·shekhar, "moon-crested" — as does NASA's
Chandra X-ray Observatory, named in his
honour, which exists to image the invisible sky. Reading what's present but unseen is the whole job.
(This project is not affiliated with or endorsed by NASA.)
The engines under the hood carry their own names:
-
rosettapowersshow,inject, andeject. Named for the Rosetta Stone, which carries one message in several scripts so a reader of any one of them can understand it. This engine does the same for a generation recipe: it takes what ComfyUI wrote in its own dialect and re-expresses it in the dialect CivitAI and SD Prompt Reader read fluently. (No relation to Apple's Rosetta.) -
concordancepowerssearch. A concordance is an alphabetical index of the words in a text or corpus together with where each one occurs — biblical and Shakespearean concordances are the classic examples. Searching the prompts across a folder of images is the same operation over a corpus of pictures. It only reads — its report goes to your terminal, never into the files — which is why it isn't calledscribe. -
palimpsestpowersscrub. A palimpsest is a manuscript page whose original writing was scraped or washed off so the surface could be reused — yet traces of the older text remain, legible to anyone who looks closely.scrubdoes the same to an image: the picture and the prompt prose are washed away, but the graph's wiring stays behind — enough to reproduce a parsing bug, without carrying anything personal.
pipx install chandraAnd later, to uninstall:
pipx uninstall chandrachandra supports tab-completion via argcomplete. Enable it
once by adding this to your ~/.bashrc (or ~/.zshrc):
eval "$(register-python-argcomplete chandra)"Open a new shell (or source the file) and chandra <TAB> will complete subcommands and flags.
register-python-argcomplete ships with argcomplete. If chandra is installed inside a virtualenv, the
helper lives there too — to have it on PATH in every shell, install argcomplete globally with
pipx install argcomplete.
The global activate-global-python-argcomplete hook does not pick up chandra: the installed
console-script wrapper doesn't carry argcomplete's # PYTHON_ARGCOMPLETE_OK marker, so per-command
registration as above is the reliable way.
To disable it: remove the eval line from your shell rc — and, to drop it from the current
shell immediately, run complete -r chandra. If you installed argcomplete solely for this,
pipx uninstall argcomplete.
Found a workflow chandra doesn't parse correctly? Bug reports (with an example image) and pull
requests are welcome — see CONTRIBUTING.md.
Two things up front: you can run chandra scrub your.png to produce an anonymized skeleton
(no image, no prompt text, just the graph wiring that reproduces the bug) to attach instead
of the original; and please keep any example images SFW (character art is fine), since
the issue tracker is public.
If you are interested in the technical design, architectural briefs live under briefs/.