chandra 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. chandra-0.1.0/LICENSE.md +24 -0
  2. chandra-0.1.0/PKG-INFO +274 -0
  3. chandra-0.1.0/README.md +246 -0
  4. chandra-0.1.0/chandra/__init__.py +30 -0
  5. chandra-0.1.0/chandra/analyze.py +451 -0
  6. chandra-0.1.0/chandra/cli.py +68 -0
  7. chandra-0.1.0/chandra/concordance.py +233 -0
  8. chandra-0.1.0/chandra/hashing.py +144 -0
  9. chandra-0.1.0/chandra/inputs.py +51 -0
  10. chandra-0.1.0/chandra/palimpsest.py +211 -0
  11. chandra-0.1.0/chandra/pngchunks.py +265 -0
  12. chandra-0.1.0/chandra/rosetta.py +256 -0
  13. chandra-0.1.0/chandra/synthesize.py +115 -0
  14. chandra-0.1.0/chandra/xmp.py +56 -0
  15. chandra-0.1.0/pyproject.toml +128 -0
  16. chandra-0.1.0/tests/fixtures/chroma1-hd-img2img.png +0 -0
  17. chandra-0.1.0/tests/fixtures/chroma1-hd-inpaint.png +0 -0
  18. chandra-0.1.0/tests/fixtures/chroma1-hd-txt2img.png +0 -0
  19. chandra-0.1.0/tests/fixtures/ernie-img2img.png +0 -0
  20. chandra-0.1.0/tests/fixtures/ernie-inpaint.png +0 -0
  21. chandra-0.1.0/tests/fixtures/ernie-txt2img.png +0 -0
  22. chandra-0.1.0/tests/fixtures/flux2-edit-inpaint.png +0 -0
  23. chandra-0.1.0/tests/fixtures/flux2-edit.png +0 -0
  24. chandra-0.1.0/tests/fixtures/flux2-img2img.png +0 -0
  25. chandra-0.1.0/tests/fixtures/flux2-inpaint.png +0 -0
  26. chandra-0.1.0/tests/fixtures/flux2-txt2img.png +0 -0
  27. chandra-0.1.0/tests/fixtures/illustrious-img2img.png +0 -0
  28. chandra-0.1.0/tests/fixtures/illustrious-inpaint.png +0 -0
  29. chandra-0.1.0/tests/fixtures/illustrious-txt2img.png +0 -0
  30. chandra-0.1.0/tests/fixtures/qwen-edit-2511-basic.png +0 -0
  31. chandra-0.1.0/tests/fixtures/qwen-edit-2511-inpaint.png +0 -0
  32. chandra-0.1.0/tests/fixtures/qwen2512-img2img.png +0 -0
  33. chandra-0.1.0/tests/fixtures/qwen2512-inpaint.png +0 -0
  34. chandra-0.1.0/tests/fixtures/qwen2512-txt2img.png +0 -0
  35. chandra-0.1.0/tests/fixtures/z-image-img2img.png +0 -0
  36. chandra-0.1.0/tests/fixtures/z-image-inpaint.png +0 -0
  37. chandra-0.1.0/tests/fixtures/z-image-txt2img.png +0 -0
  38. chandra-0.1.0/tests/test_analyze.py +251 -0
  39. chandra-0.1.0/tests/test_cli.py +68 -0
  40. chandra-0.1.0/tests/test_eject.py +113 -0
  41. chandra-0.1.0/tests/test_fixtures.py +102 -0
  42. chandra-0.1.0/tests/test_hashing.py +143 -0
  43. chandra-0.1.0/tests/test_inputs.py +86 -0
  44. chandra-0.1.0/tests/test_palimpsest.py +162 -0
  45. chandra-0.1.0/tests/test_pngchunks.py +233 -0
  46. chandra-0.1.0/tests/test_search.py +198 -0
  47. chandra-0.1.0/tests/test_synthesize.py +254 -0
  48. chandra-0.1.0/tests/test_xmp.py +49 -0
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, Juha Jeronen
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
chandra-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.4
2
+ Name: chandra
3
+ Version: 0.1.0
4
+ Summary: Tools for working with the metadata that image generators embed in their output.
5
+ Keywords: comfyui,civitai,stable-diffusion,metadata,png,a1111,sd-prompt-reader
6
+ Author-Email: Juha Jeronen <juha.jeronen@jamk.fi>
7
+ License-Expression: BSD-2-Clause
8
+ License-File: LICENSE.md
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Multimedia :: Graphics
19
+ Classifier: Topic :: Utilities
20
+ Project-URL: Homepage, https://github.com/Technologicat/chandra
21
+ Project-URL: Repository, https://github.com/Technologicat/chandra
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: simpleeval>=0.9.13
24
+ Requires-Dist: argcomplete>=3.0
25
+ Requires-Dist: chardet>=5.0
26
+ Requires-Dist: colorama>=0.4.6
27
+ Description-Content-Type: text/markdown
28
+
29
+ # chandra
30
+
31
+ Tools for working with the metadata that AI image generators embed in their output.
32
+
33
+ ![100% Python](https://img.shields.io/github/languages/top/Technologicat/chandra) ![supported language versions](https://img.shields.io/pypi/pyversions/chandra) ![supported implementations](https://img.shields.io/pypi/implementation/chandra) ![CI status](https://img.shields.io/github/actions/workflow/status/Technologicat/chandra/ci.yml?branch=main) [![codecov](https://codecov.io/gh/Technologicat/chandra/branch/main/graph/badge.svg)](https://codecov.io/gh/Technologicat/chandra)
34
+ ![version on PyPI](https://img.shields.io/pypi/v/chandra) ![PyPI package format](https://img.shields.io/pypi/format/chandra) ![dependency status](https://img.shields.io/librariesio/github/Technologicat/chandra)
35
+ ![license: BSD 2-Clause](https://img.shields.io/pypi/l/chandra) ![open issues](https://img.shields.io/github/issues/Technologicat/chandra) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](http://makeapullrequest.com/)
36
+
37
+ For my stance on AI contributions, see the [collaboration guidelines](https://github.com/Technologicat/substrate-independent/blob/main/collaboration.md).
38
+
39
+ We use [semantic versioning](https://semver.org/).
40
+
41
+ ## Overview
42
+
43
+ Everything is one command, **`chandra`**, with these subcommands:
44
+
45
+ | Command | What it does |
46
+ |---|---|
47
+ | `chandra show <png…>` | Read a ComfyUI image and **print** the [AUTOMATIC1111](https://github.com/AUTOMATIC1111/stable-diffusion-webui)/[SD-Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) metadata that `chandra inject` *would* write. Read-only. |
48
+ | `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](https://civitai.com) on upload, and [SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader) locally. |
49
+ | `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. |
50
+ | `chandra search <terms…>` | Search the prompts embedded across a directory tree of generated images. |
51
+ | `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. |
52
+
53
+ Reading and writing are deliberately separate commands: `show` never modifies anything, and writing
54
+ only happens when you explicitly ask for `inject` (or `eject`, to undo it).
55
+
56
+ ```bash
57
+ chandra show image.png # preview the synthesized metadata
58
+ chandra inject *.png # write metadata into a batch, in place
59
+ chandra inject imgs/ # …or hand it a directory (recursed)
60
+ chandra eject *.png # remove that metadata again (inverse of inject)
61
+ chandra search starfleet captain # find images whose prompt mentions a starfleet captain
62
+ chandra search catgirl -d imgs | chandra search -n blurry # chain searches to refine the result set
63
+ chandra search catgirl -d imgs | chandra inject # inject only the images a search found
64
+ ```
65
+
66
+ Every command takes the same inputs: files and/or directories (directories are recursed), or a
67
+ list of paths piped in on stdin, one per line — which is what lets a `search` feed `show`, `inject`,
68
+ or `eject`. `search` takes its roots with `-d` (its positional arguments are the search terms); the
69
+ others take them as positional arguments. With nothing to act on, each command prints a short usage
70
+ instead of guessing: bare `chandra search` asks for terms, bare `chandra show` / `inject` / `eject`
71
+ ask for paths. The one convenience is that `search` (once it has terms) defaults its search root to
72
+ the current directory; the writing commands never default to the cwd — so a bare `chandra inject` or
73
+ `chandra eject` can't modify files there by surprise.
74
+
75
+ Why this is useful: many services and apps such as CivitAI and SD Prompt Reader mostly *punt* on
76
+ analyzing ComfyUI workflows — a trivial txt2img graph is sometimes captured, but img2img, inpaint,
77
+ edit-mode, LoRA chains, and non-standard loaders are not. `chandra` walks the embedded ComfyUI graph
78
+ itself, reconstructs the recipe, and re-expresses it in the one format those tools read robustly.
79
+
80
+ ## Injecting metadata (`inject`)
81
+
82
+ `inject` writes the recipe straight into the PNG, in place and losslessly — the original ComfyUI
83
+ `prompt`/`workflow` chunks are never touched. It writes two independent layers, both on by default: a
84
+ machine-readable A1111/SD-Forge `parameters` chunk (what CivitAI and SD Prompt Reader read) and an
85
+ XMP `dc:description` (what general image viewers show). The two sections below cover what each layer
86
+ is for — auto-linking your resources on CivitAI, and seeing the recipe in an everyday image viewer.
87
+
88
+ ### Auto-linking resources on CivitAI (`--hash`)
89
+
90
+ By default the checkpoint and LoRAs are named as plain text — readable by a human and by SD Prompt
91
+ Reader, but invisible to CivitAI, which keys its resource detection off hashes and surfaces nothing
92
+ without them. Add `--hash` (to `show` or `inject`) and `chandra` computes the AutoV2 hash
93
+ (`sha256[:10]`) of each file and emits `Model hash:` and `Lora hashes:`, which CivitAI matches to the
94
+ corresponding resource pages on upload:
95
+
96
+ ```bash
97
+ chandra inject *.png --hash --models-dir ~/ComfyUI/models
98
+ ```
99
+
100
+ Hashing needs the actual files, so you need to tell `chandra` where they live — either with
101
+ `--models-dir DIR` (repeatable) or via the **`CHANDRA_MODELS_DIR`** environment variable,
102
+ a `PATH`-style list of directories (colon-separated on Linux/macOS, semicolon on Windows):
103
+
104
+ ```bash
105
+ export CHANDRA_MODELS_DIR=~/ComfyUI/models:~/extra/loras
106
+ chandra inject *.png --hash # picks up the dirs from the environment
107
+ ```
108
+
109
+ On Linux, to set the environment variable persistently, place the `export` command in your `.bashrc`.
110
+
111
+ The directories are indexed once and hashes are cached (keyed by path, size, and mtime), so a
112
+ multi-GB checkpoint shared across a batch is hashed only the first time. Only the checkpoint and
113
+ LoRAs auto-link on CivitAI — its detection covers nothing else.
114
+
115
+ The recipe also records the **VAE** (`VAE:`, plus `VAE hash:` under `--hash`) and any separate
116
+ **text encoders** — common on modern models (Flux, Qwen, …), often an LLM — as SD-Forge `Module N`
117
+ fields. CivitAI ignores both, but they're standard, faithful metadata that SD Prompt Reader, general
118
+ image viewers, and `chandra show --recipe` display; the text encoder in particular materially shapes
119
+ the result, so it's worth recording. Text encoders aren't hashed (no standard infotext hash field).
120
+
121
+ ### Seeing the recipe in a general image viewer
122
+
123
+ `inject` also embeds a clean, human-readable rendering of the recipe — the same information as
124
+ `chandra show --recipe` — as an XMP `dc:description`. So a general image viewer that reads standard
125
+ metadata (e.g. [Pix](https://github.com/linuxmint/pix), the Linux Mint viewer) shows the prompt and
126
+ settings in its **Description** caption, no SD software needed — often enough to skip opening a
127
+ dedicated prompt reader just to glance at what made an image. This is on by default; pass `--no-xmp`
128
+ to write only the machine-oriented `parameters` chunk. The two layers are independent and both
129
+ lossless — the original ComfyUI `prompt`/`workflow` chunks are never touched.
130
+
131
+ LoRAs differ between the layers, by design. The machine `parameters` chunk renders them in A1111's
132
+ inline `<lora:name:strength>` notation — that's the format's idiom, and the only standard place a
133
+ LoRA's *strength* is recorded.
134
+
135
+ ComfyUI itself never writes LoRAs into the prompt text, so the human-readable views keep the prose
136
+ clean and list them separately (`LoRA: name (strength X)` in the description and `chandra show --recipe`).
137
+ The inlined-into-prompt form is a data interchange convention.
138
+
139
+ ## Undoing an inject (`eject`)
140
+
141
+ Changed your mind? `chandra eject` is the inverse of `inject`: it removes the `parameters` chunk and
142
+ the XMP description, leaving the original ComfyUI `prompt`/`workflow` chunks byte-for-byte intact — an
143
+ `inject` followed by an `eject` restores the file exactly (byte-identical to the original, with the
144
+ same `md5sum`).
145
+
146
+ ```bash
147
+ chandra eject *.png # remove chandra's metadata from a batch, in place
148
+ ```
149
+
150
+ By default `eject` removes **only metadata chandra wrote** — both layers carry a `chandra-rosetta`
151
+ stamp (the `Version:` field of the `parameters` chunk and the `x:xmptk` attribute of the XMP packet),
152
+ and anything unstamped is left alone, so it won't clobber a `parameters` block from A1111/Forge or an
153
+ XMP caption some other tool added. Two flags adjust that: `--no-xmp` removes only the `parameters`
154
+ chunk and leaves the XMP description; `--force` removes the `parameters` chunk and XMP regardless of
155
+ who wrote them.
156
+
157
+ ## Searching (`search`)
158
+
159
+ `chandra search` builds boolean queries from three primitives — no special syntax or metacharacters:
160
+
161
+ | | flag | example |
162
+ |---|---|---|
163
+ | **AND** | *(default)* | `chandra search cat photo` — prompt contains both fragments |
164
+ | **OR** | `--or` (`--any`) | `chandra search --or captain admiral` — either fragment |
165
+ | **NOT** | `--not` (`--invert`, `-v`) | `chandra search --not klingon` — prompt lacks the fragment |
166
+
167
+ Fragments match as **substrings**, order-independent: `cat photo` also matches `photocatalytic`.
168
+
169
+ Fragments are **smart-cased**: an all-lowercase fragment is case-insensitive, a fragment with
170
+ any uppercase letter is case-sensitive. The flag `-i` forces case-insensitive.
171
+
172
+ `chandra search` is a *nix-style filter — matching paths go to stdout, and when input is piped,
173
+ it reads candidate paths from stdin. So **chaining refines**: each stage filters the previous
174
+ stage's results (set intersection), which gives full boolean in conjunctive normal form:
175
+
176
+ ```bash
177
+ chandra search starship | chandra search --or captain admiral | chandra search --not klingon
178
+ # → starship AND (captain OR admiral) AND (NOT klingon)
179
+ ```
180
+
181
+ …and results compose with the rest of the shell:
182
+
183
+ ```bash
184
+ chandra search wizard -d imgs | wc -l # count matches
185
+ chandra search cat -d imgs | xargs -d'\n' cp -t picks/ # copy matches elsewhere
186
+ chandra search catgirl -d imgs | fzf # pick one interactively
187
+ ```
188
+
189
+ More flags:
190
+
191
+ - `-p` / `-n` search the positive / negative prompt only,
192
+ - `--exact` matches the whole query as one contiguous phrase instead of fragments,
193
+ - `-C` / `--context` prints a highlighted snippet of each match, colorized on a terminal,
194
+ - `--dirs-only` prints matching directories instead of files, and
195
+ - `-d DIR` sets the search roots, repeatable; default is piped stdin, else the current directory.
196
+
197
+ ## On the names
198
+
199
+ **`chandra`** is Sanskrit for *the moon* (चन्द्र), the Hindu lunar deity. The metadata this tool
200
+ recovers is an image's nocturnal layer — dimmer than the bright pixels, easy to overlook, but there
201
+ to be read once you look for it. The name rewards a second glance: the astrophysicist *Subrahmanyan
202
+ Chandrasekhar* (of the [Chandrasekhar limit](https://en.wikipedia.org/wiki/Chandrasekhar_limit))
203
+ carries the same root — *Chandra·shekhar*, "moon-crested" — as does NASA's
204
+ [Chandra X-ray Observatory](https://en.wikipedia.org/wiki/Chandra_X-ray_Observatory), named in his
205
+ honour, which exists to image the *invisible* sky. Reading what's present but unseen is the whole job.
206
+ *(This project is not affiliated with or endorsed by NASA.)*
207
+
208
+ The engines under the hood carry their own names:
209
+
210
+ - **`rosetta`** powers `show`, `inject`, and `eject`. Named for the
211
+ [Rosetta Stone](https://en.wikipedia.org/wiki/Rosetta_Stone), which carries one message in several
212
+ scripts so a reader of any one of them can understand it. This engine does the same for a
213
+ generation recipe: it takes what ComfyUI wrote in its own dialect and re-expresses it in the
214
+ dialect CivitAI and SD Prompt Reader read fluently. (No relation to Apple's Rosetta.)
215
+
216
+ - **`concordance`** powers `search`. A
217
+ [concordance](https://en.wikipedia.org/wiki/Concordance_(publishing)) is an alphabetical index of
218
+ the words in a text or corpus together with where each one occurs — biblical and Shakespearean
219
+ concordances are the classic examples. Searching the prompts across a folder of images is the same
220
+ operation over a corpus of pictures. It only reads — its report goes to your terminal, never into
221
+ the files — which is why it isn't called `scribe`.
222
+
223
+ - **`palimpsest`** powers `scrub`. A [palimpsest](https://en.wikipedia.org/wiki/Palimpsest) is a
224
+ manuscript page whose original writing was scraped or washed off so the surface could be reused —
225
+ yet traces of the older text remain, legible to anyone who looks closely. `scrub` does the same to
226
+ an image: the picture and the prompt prose are washed away, but the graph's wiring stays behind —
227
+ enough to reproduce a parsing bug, without carrying anything personal.
228
+
229
+ ## Installation
230
+
231
+ ```bash
232
+ pipx install chandra
233
+ ```
234
+
235
+ And later, to uninstall:
236
+
237
+ ```bash
238
+ pipx uninstall chandra
239
+ ```
240
+
241
+ ## Shell completion (optional)
242
+
243
+ `chandra` supports tab-completion via [argcomplete](https://github.com/kislyuk/argcomplete). Enable it
244
+ once by adding this to your `~/.bashrc` (or `~/.zshrc`):
245
+
246
+ ```bash
247
+ eval "$(register-python-argcomplete chandra)"
248
+ ```
249
+
250
+ Open a new shell (or `source` the file) and `chandra <TAB>` will complete subcommands and flags.
251
+
252
+ `register-python-argcomplete` ships with argcomplete. If `chandra` is installed inside a virtualenv, the
253
+ helper lives there too — to have it on `PATH` in every shell, install argcomplete globally with
254
+ `pipx install argcomplete`.
255
+
256
+ The *global* `activate-global-python-argcomplete` hook does **not** pick up `chandra`: the installed
257
+ console-script wrapper doesn't carry argcomplete's `# PYTHON_ARGCOMPLETE_OK` marker, so per-command
258
+ registration as above is the reliable way.
259
+
260
+ **To disable it:** remove the `eval` line from your shell rc — and, to drop it from the current
261
+ shell immediately, run `complete -r chandra`. If you installed argcomplete solely for this,
262
+ `pipx uninstall argcomplete`.
263
+
264
+ ## Contributing
265
+
266
+ Found a workflow `chandra` doesn't parse correctly? Bug reports (with an example image) and pull
267
+ requests are welcome — see [`CONTRIBUTING.md`](CONTRIBUTING.md).
268
+
269
+ Two things up front: you can run `chandra scrub your.png` to produce an anonymized skeleton
270
+ (no image, no prompt text, just the graph wiring that reproduces the bug) to attach instead
271
+ of the original; and please keep any example images **SFW** (character art is fine), since
272
+ the issue tracker is public.
273
+
274
+ If you are interested in the technical design, architectural briefs live under [`briefs/`](briefs/).
@@ -0,0 +1,246 @@
1
+ # chandra
2
+
3
+ Tools for working with the metadata that AI image generators embed in their output.
4
+
5
+ ![100% Python](https://img.shields.io/github/languages/top/Technologicat/chandra) ![supported language versions](https://img.shields.io/pypi/pyversions/chandra) ![supported implementations](https://img.shields.io/pypi/implementation/chandra) ![CI status](https://img.shields.io/github/actions/workflow/status/Technologicat/chandra/ci.yml?branch=main) [![codecov](https://codecov.io/gh/Technologicat/chandra/branch/main/graph/badge.svg)](https://codecov.io/gh/Technologicat/chandra)
6
+ ![version on PyPI](https://img.shields.io/pypi/v/chandra) ![PyPI package format](https://img.shields.io/pypi/format/chandra) ![dependency status](https://img.shields.io/librariesio/github/Technologicat/chandra)
7
+ ![license: BSD 2-Clause](https://img.shields.io/pypi/l/chandra) ![open issues](https://img.shields.io/github/issues/Technologicat/chandra) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](http://makeapullrequest.com/)
8
+
9
+ For my stance on AI contributions, see the [collaboration guidelines](https://github.com/Technologicat/substrate-independent/blob/main/collaboration.md).
10
+
11
+ We use [semantic versioning](https://semver.org/).
12
+
13
+ ## Overview
14
+
15
+ Everything is one command, **`chandra`**, with these subcommands:
16
+
17
+ | Command | What it does |
18
+ |---|---|
19
+ | `chandra show <png…>` | Read a ComfyUI image and **print** the [AUTOMATIC1111](https://github.com/AUTOMATIC1111/stable-diffusion-webui)/[SD-Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) metadata that `chandra inject` *would* write. Read-only. |
20
+ | `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](https://civitai.com) on upload, and [SD Prompt Reader](https://github.com/receyuki/stable-diffusion-prompt-reader) locally. |
21
+ | `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. |
22
+ | `chandra search <terms…>` | Search the prompts embedded across a directory tree of generated images. |
23
+ | `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. |
24
+
25
+ Reading and writing are deliberately separate commands: `show` never modifies anything, and writing
26
+ only happens when you explicitly ask for `inject` (or `eject`, to undo it).
27
+
28
+ ```bash
29
+ chandra show image.png # preview the synthesized metadata
30
+ chandra inject *.png # write metadata into a batch, in place
31
+ chandra inject imgs/ # …or hand it a directory (recursed)
32
+ chandra eject *.png # remove that metadata again (inverse of inject)
33
+ chandra search starfleet captain # find images whose prompt mentions a starfleet captain
34
+ chandra search catgirl -d imgs | chandra search -n blurry # chain searches to refine the result set
35
+ chandra search catgirl -d imgs | chandra inject # inject only the images a search found
36
+ ```
37
+
38
+ Every command takes the same inputs: files and/or directories (directories are recursed), or a
39
+ list of paths piped in on stdin, one per line — which is what lets a `search` feed `show`, `inject`,
40
+ or `eject`. `search` takes its roots with `-d` (its positional arguments are the search terms); the
41
+ others take them as positional arguments. With nothing to act on, each command prints a short usage
42
+ instead of guessing: bare `chandra search` asks for terms, bare `chandra show` / `inject` / `eject`
43
+ ask for paths. The one convenience is that `search` (once it has terms) defaults its search root to
44
+ the current directory; the writing commands never default to the cwd — so a bare `chandra inject` or
45
+ `chandra eject` can't modify files there by surprise.
46
+
47
+ Why this is useful: many services and apps such as CivitAI and SD Prompt Reader mostly *punt* on
48
+ analyzing ComfyUI workflows — a trivial txt2img graph is sometimes captured, but img2img, inpaint,
49
+ edit-mode, LoRA chains, and non-standard loaders are not. `chandra` walks the embedded ComfyUI graph
50
+ itself, reconstructs the recipe, and re-expresses it in the one format those tools read robustly.
51
+
52
+ ## Injecting metadata (`inject`)
53
+
54
+ `inject` writes the recipe straight into the PNG, in place and losslessly — the original ComfyUI
55
+ `prompt`/`workflow` chunks are never touched. It writes two independent layers, both on by default: a
56
+ machine-readable A1111/SD-Forge `parameters` chunk (what CivitAI and SD Prompt Reader read) and an
57
+ XMP `dc:description` (what general image viewers show). The two sections below cover what each layer
58
+ is for — auto-linking your resources on CivitAI, and seeing the recipe in an everyday image viewer.
59
+
60
+ ### Auto-linking resources on CivitAI (`--hash`)
61
+
62
+ By default the checkpoint and LoRAs are named as plain text — readable by a human and by SD Prompt
63
+ Reader, but invisible to CivitAI, which keys its resource detection off hashes and surfaces nothing
64
+ without them. Add `--hash` (to `show` or `inject`) and `chandra` computes the AutoV2 hash
65
+ (`sha256[:10]`) of each file and emits `Model hash:` and `Lora hashes:`, which CivitAI matches to the
66
+ corresponding resource pages on upload:
67
+
68
+ ```bash
69
+ chandra inject *.png --hash --models-dir ~/ComfyUI/models
70
+ ```
71
+
72
+ Hashing needs the actual files, so you need to tell `chandra` where they live — either with
73
+ `--models-dir DIR` (repeatable) or via the **`CHANDRA_MODELS_DIR`** environment variable,
74
+ a `PATH`-style list of directories (colon-separated on Linux/macOS, semicolon on Windows):
75
+
76
+ ```bash
77
+ export CHANDRA_MODELS_DIR=~/ComfyUI/models:~/extra/loras
78
+ chandra inject *.png --hash # picks up the dirs from the environment
79
+ ```
80
+
81
+ On Linux, to set the environment variable persistently, place the `export` command in your `.bashrc`.
82
+
83
+ The directories are indexed once and hashes are cached (keyed by path, size, and mtime), so a
84
+ multi-GB checkpoint shared across a batch is hashed only the first time. Only the checkpoint and
85
+ LoRAs auto-link on CivitAI — its detection covers nothing else.
86
+
87
+ The recipe also records the **VAE** (`VAE:`, plus `VAE hash:` under `--hash`) and any separate
88
+ **text encoders** — common on modern models (Flux, Qwen, …), often an LLM — as SD-Forge `Module N`
89
+ fields. CivitAI ignores both, but they're standard, faithful metadata that SD Prompt Reader, general
90
+ image viewers, and `chandra show --recipe` display; the text encoder in particular materially shapes
91
+ the result, so it's worth recording. Text encoders aren't hashed (no standard infotext hash field).
92
+
93
+ ### Seeing the recipe in a general image viewer
94
+
95
+ `inject` also embeds a clean, human-readable rendering of the recipe — the same information as
96
+ `chandra show --recipe` — as an XMP `dc:description`. So a general image viewer that reads standard
97
+ metadata (e.g. [Pix](https://github.com/linuxmint/pix), the Linux Mint viewer) shows the prompt and
98
+ settings in its **Description** caption, no SD software needed — often enough to skip opening a
99
+ dedicated prompt reader just to glance at what made an image. This is on by default; pass `--no-xmp`
100
+ to write only the machine-oriented `parameters` chunk. The two layers are independent and both
101
+ lossless — the original ComfyUI `prompt`/`workflow` chunks are never touched.
102
+
103
+ LoRAs differ between the layers, by design. The machine `parameters` chunk renders them in A1111's
104
+ inline `<lora:name:strength>` notation — that's the format's idiom, and the only standard place a
105
+ LoRA's *strength* is recorded.
106
+
107
+ ComfyUI itself never writes LoRAs into the prompt text, so the human-readable views keep the prose
108
+ clean and list them separately (`LoRA: name (strength X)` in the description and `chandra show --recipe`).
109
+ The inlined-into-prompt form is a data interchange convention.
110
+
111
+ ## Undoing an inject (`eject`)
112
+
113
+ Changed your mind? `chandra eject` is the inverse of `inject`: it removes the `parameters` chunk and
114
+ the XMP description, leaving the original ComfyUI `prompt`/`workflow` chunks byte-for-byte intact — an
115
+ `inject` followed by an `eject` restores the file exactly (byte-identical to the original, with the
116
+ same `md5sum`).
117
+
118
+ ```bash
119
+ chandra eject *.png # remove chandra's metadata from a batch, in place
120
+ ```
121
+
122
+ By default `eject` removes **only metadata chandra wrote** — both layers carry a `chandra-rosetta`
123
+ stamp (the `Version:` field of the `parameters` chunk and the `x:xmptk` attribute of the XMP packet),
124
+ and anything unstamped is left alone, so it won't clobber a `parameters` block from A1111/Forge or an
125
+ XMP caption some other tool added. Two flags adjust that: `--no-xmp` removes only the `parameters`
126
+ chunk and leaves the XMP description; `--force` removes the `parameters` chunk and XMP regardless of
127
+ who wrote them.
128
+
129
+ ## Searching (`search`)
130
+
131
+ `chandra search` builds boolean queries from three primitives — no special syntax or metacharacters:
132
+
133
+ | | flag | example |
134
+ |---|---|---|
135
+ | **AND** | *(default)* | `chandra search cat photo` — prompt contains both fragments |
136
+ | **OR** | `--or` (`--any`) | `chandra search --or captain admiral` — either fragment |
137
+ | **NOT** | `--not` (`--invert`, `-v`) | `chandra search --not klingon` — prompt lacks the fragment |
138
+
139
+ Fragments match as **substrings**, order-independent: `cat photo` also matches `photocatalytic`.
140
+
141
+ Fragments are **smart-cased**: an all-lowercase fragment is case-insensitive, a fragment with
142
+ any uppercase letter is case-sensitive. The flag `-i` forces case-insensitive.
143
+
144
+ `chandra search` is a *nix-style filter — matching paths go to stdout, and when input is piped,
145
+ it reads candidate paths from stdin. So **chaining refines**: each stage filters the previous
146
+ stage's results (set intersection), which gives full boolean in conjunctive normal form:
147
+
148
+ ```bash
149
+ chandra search starship | chandra search --or captain admiral | chandra search --not klingon
150
+ # → starship AND (captain OR admiral) AND (NOT klingon)
151
+ ```
152
+
153
+ …and results compose with the rest of the shell:
154
+
155
+ ```bash
156
+ chandra search wizard -d imgs | wc -l # count matches
157
+ chandra search cat -d imgs | xargs -d'\n' cp -t picks/ # copy matches elsewhere
158
+ chandra search catgirl -d imgs | fzf # pick one interactively
159
+ ```
160
+
161
+ More flags:
162
+
163
+ - `-p` / `-n` search the positive / negative prompt only,
164
+ - `--exact` matches the whole query as one contiguous phrase instead of fragments,
165
+ - `-C` / `--context` prints a highlighted snippet of each match, colorized on a terminal,
166
+ - `--dirs-only` prints matching directories instead of files, and
167
+ - `-d DIR` sets the search roots, repeatable; default is piped stdin, else the current directory.
168
+
169
+ ## On the names
170
+
171
+ **`chandra`** is Sanskrit for *the moon* (चन्द्र), the Hindu lunar deity. The metadata this tool
172
+ recovers is an image's nocturnal layer — dimmer than the bright pixels, easy to overlook, but there
173
+ to be read once you look for it. The name rewards a second glance: the astrophysicist *Subrahmanyan
174
+ Chandrasekhar* (of the [Chandrasekhar limit](https://en.wikipedia.org/wiki/Chandrasekhar_limit))
175
+ carries the same root — *Chandra·shekhar*, "moon-crested" — as does NASA's
176
+ [Chandra X-ray Observatory](https://en.wikipedia.org/wiki/Chandra_X-ray_Observatory), named in his
177
+ honour, which exists to image the *invisible* sky. Reading what's present but unseen is the whole job.
178
+ *(This project is not affiliated with or endorsed by NASA.)*
179
+
180
+ The engines under the hood carry their own names:
181
+
182
+ - **`rosetta`** powers `show`, `inject`, and `eject`. Named for the
183
+ [Rosetta Stone](https://en.wikipedia.org/wiki/Rosetta_Stone), which carries one message in several
184
+ scripts so a reader of any one of them can understand it. This engine does the same for a
185
+ generation recipe: it takes what ComfyUI wrote in its own dialect and re-expresses it in the
186
+ dialect CivitAI and SD Prompt Reader read fluently. (No relation to Apple's Rosetta.)
187
+
188
+ - **`concordance`** powers `search`. A
189
+ [concordance](https://en.wikipedia.org/wiki/Concordance_(publishing)) is an alphabetical index of
190
+ the words in a text or corpus together with where each one occurs — biblical and Shakespearean
191
+ concordances are the classic examples. Searching the prompts across a folder of images is the same
192
+ operation over a corpus of pictures. It only reads — its report goes to your terminal, never into
193
+ the files — which is why it isn't called `scribe`.
194
+
195
+ - **`palimpsest`** powers `scrub`. A [palimpsest](https://en.wikipedia.org/wiki/Palimpsest) is a
196
+ manuscript page whose original writing was scraped or washed off so the surface could be reused —
197
+ yet traces of the older text remain, legible to anyone who looks closely. `scrub` does the same to
198
+ an image: the picture and the prompt prose are washed away, but the graph's wiring stays behind —
199
+ enough to reproduce a parsing bug, without carrying anything personal.
200
+
201
+ ## Installation
202
+
203
+ ```bash
204
+ pipx install chandra
205
+ ```
206
+
207
+ And later, to uninstall:
208
+
209
+ ```bash
210
+ pipx uninstall chandra
211
+ ```
212
+
213
+ ## Shell completion (optional)
214
+
215
+ `chandra` supports tab-completion via [argcomplete](https://github.com/kislyuk/argcomplete). Enable it
216
+ once by adding this to your `~/.bashrc` (or `~/.zshrc`):
217
+
218
+ ```bash
219
+ eval "$(register-python-argcomplete chandra)"
220
+ ```
221
+
222
+ Open a new shell (or `source` the file) and `chandra <TAB>` will complete subcommands and flags.
223
+
224
+ `register-python-argcomplete` ships with argcomplete. If `chandra` is installed inside a virtualenv, the
225
+ helper lives there too — to have it on `PATH` in every shell, install argcomplete globally with
226
+ `pipx install argcomplete`.
227
+
228
+ The *global* `activate-global-python-argcomplete` hook does **not** pick up `chandra`: the installed
229
+ console-script wrapper doesn't carry argcomplete's `# PYTHON_ARGCOMPLETE_OK` marker, so per-command
230
+ registration as above is the reliable way.
231
+
232
+ **To disable it:** remove the `eval` line from your shell rc — and, to drop it from the current
233
+ shell immediately, run `complete -r chandra`. If you installed argcomplete solely for this,
234
+ `pipx uninstall argcomplete`.
235
+
236
+ ## Contributing
237
+
238
+ Found a workflow `chandra` doesn't parse correctly? Bug reports (with an example image) and pull
239
+ requests are welcome — see [`CONTRIBUTING.md`](CONTRIBUTING.md).
240
+
241
+ Two things up front: you can run `chandra scrub your.png` to produce an anonymized skeleton
242
+ (no image, no prompt text, just the graph wiring that reproduces the bug) to attach instead
243
+ of the original; and please keep any example images **SFW** (character art is fine), since
244
+ the issue tracker is public.
245
+
246
+ If you are interested in the technical design, architectural briefs live under [`briefs/`](briefs/).
@@ -0,0 +1,30 @@
1
+ """chandra — tools for the metadata image generators embed in their output.
2
+
3
+ Everything is dispatched through a single `chandra` entry point:
4
+
5
+ - ``chandra show`` — print the A1111/CivitAI-compatible metadata derived from an embedded
6
+ ComfyUI workflow (read-only).
7
+ - ``chandra inject`` — write that metadata into the image(s) in place, so they're recognized by
8
+ services that don't analyze ComfyUI graphs themselves.
9
+ - ``chandra eject`` — remove that metadata again (the inverse of inject), restoring the image to
10
+ its pre-inject state.
11
+ - ``chandra search`` — search the prompts embedded across a directory tree of generated images.
12
+ - ``chandra scrub`` — strip a ComfyUI image to a de-branded, shareable skeleton.
13
+
14
+ Three engines do the work: `rosetta` (analyze + synthesize + inject/eject, behind show/inject/eject),
15
+ `concordance` (behind search), and `palimpsest` (behind scrub). See the README for the naming lore.
16
+ """
17
+
18
+ from importlib.metadata import PackageNotFoundError, version
19
+
20
+ try:
21
+ __version__ = version("chandra")
22
+ except PackageNotFoundError: # running from a source tree without an installed dist
23
+ __version__ = "0.0.0+unknown"
24
+
25
+ # Signature stamped into everything `inject` writes — the A1111 `Version:` field and the XMP packet's
26
+ # `x:xmptk` (toolkit) attribute — so `eject` can recognize chandra's own output and remove only that,
27
+ # never a third party's metadata. The `rosetta` suffix names the engine that writes it (README lore).
28
+ TOOL_TAG = "chandra-rosetta"
29
+
30
+ __all__ = ["__version__", "TOOL_TAG"]