Contextualises a folder of painter reference images. Point it at your library and it identifies each work with Claude's vision (via the Message Batches API, which costs half the normal rate), then builds a searchable offline wiki of the whole collection. Cataloguing never moves, renames, or writes over an image: your files are read, never touched. The one thing that ever moves a file is the sorter (menu option 5), which only acts on a plan it has shown you first, and every sort can be undone in one step.
Only wiki.html sits at the library root, next to your images — it has to,
because it references them by relative path. Every other working file lives
in a _database folder at the root, keeping the library itself tidy.
Libraries created before this layout are tidied automatically on the next
run.
| File | Where | What it is |
|---|---|---|
wiki.html |
library root | A self-contained viewer. Double-click it, no internet needed. |
catalog.json |
_database |
The master file. Everything else is generated from it. |
catalog.csv |
_database |
The same data as a spreadsheet, for Excel or Sheets. |
review_queue.csv |
_database |
Only the items where the artist attribution is an uncertain guess. |
artists.json |
_database |
The artist index: one entry per painter. Hand-editable. |
catalog.json is the single source of truth. The CSVs and wiki.html are
derived from it and can be rebuilt at any time for free (menu option 4).
Each record holds: artist, confidence, title, date, movement, medium, subject,
palette, composition notes, work-specific context, and search tags — plus
provenance: catalogued_by names the model whose eyes made the record, and
reviewed_by is set once a stronger model has re-checked it. Both show on
the card in the wiki and are searchable (type "haiku" or "fable"). Facts
about the painter (years, nationality, school, one bio) live once per
artist in artists.json, not repeated per image — see "The artist index". Artist
names found in your folder and file names are taken as ground truth; the model
only guesses when the path tells it nothing, and it reports honest confidence
when it does. Uncertain guesses are flagged for review.
You need Python 3 with two packages, and an Anthropic API key (from https://console.anthropic.com/).
pip install -r requirements.txt
Set the key in PowerShell (once, then restart the terminal):
setx ANTHROPIC_API_KEY "sk-ant-..."python catalog_refs.py
It asks for the folder to catalogue, whether to include subfolders, and what to do. One catalogue can span a whole library: if the folder you point at sits inside a folder that already has a catalogue, the tool grows that existing library instead of starting a separate one, so you can catalogue a subfolder at a time and everything lands in one master viewer at the root. You can also move an existing library to another folder (image links are fixed automatically) or start a fresh one. The menu options:
- Estimate the cost. Free, no API key used, nothing sent anywhere.
- Test run on 25 images. A small charge, to check the quality first.
- Full run. The real thing. Before it starts you choose the order — library order, or highest-resolution images first — and can give it a spend cap in dollars: it measures every image locally (free), sends only as much as the cap covers, and leaves the rest for a later run. Progress is saved batch by batch; if the run is interrupted — or your API credit runs out mid-way — run it again whenever, even next month, and it continues exactly where it stopped. Anything that failed is simply retried on the next run.
- Rebuild the viewer from existing results. Free.
- Sort catalogued images into painter folders. Shows its full plan first; nothing moves until you say so. See below.
- Undo the last sort. Replays the journal in reverse.
The sensible order is 1, then 2, then 3.
Expect roughly $0.006 per image on Sonnet batch pricing, so a 5,000 image
library is in the $15 to $30 range. Option 1 gives you the real number for
your folder before you spend anything. Model IDs and prices drift; both live
at the top of catalog_refs.py as plain editable settings, with a note of
when they were last checked.
_database/artists.json holds one entry per painter: canonical
name, birth/death years, nationality, primary school, a single 60-word bio,
and a Wikipedia title, plus an alias map that folds spelling variants
("Theodoros Ralli" / "Theodoros Rallis") onto one painter. It exists so
artist-level facts are generated once, not re-told slightly differently
on every card by that painter.
- It fills itself in automatically after each catalogue run: any painters new to the catalogue get looked up in one small text-only API call (no images, well under a cent for a whole library).
- The wiki shows the entry as an "About the painter" panel on every work by that painter, and search reaches it too — searching "Swedish" finds all works by Swedish painters even when no record mentions it.
- It is plain JSON and hand-editable. Your edits win: lookups only fill fields that are still empty, never overwrite a value.
- To backfill bios for works catalogued before the index existed, run option 3 on an already-done folder: nothing is re-sent, but the index is topped up and the wiki rebuilt.
Because the biography lives here, the per-image context field is
work-specific by instruction — which also trims the output tokens each
image pays for.
Cataloguing does the expensive looking; sorting is a cheap, deterministic file operation on top of it. Point the tool at a messy folder, catalogue it (option 2 or 3), then choose option 5. It works in three stages:
- Artist index. Painter identities come from the index above; any
names it does not know yet are looked up first. If a year is wrong, edit
artists.jsonand it stays fixed. - Plan. Every proposed move is written to
_database/sort_plan.csvand summarised. Nothing has moved yet. Open the CSV and delete any rows you disagree with, then re-run option 5 and chooseato apply the edited plan. - Apply. Files move, every operation is journaled to
_database/sort_undo.json, the catalogue's paths are rewritten, and the CSVs and wiki are rebuilt so no links break. Option 6 replays the journal in reverse.
The rules it sorts by:
- Folders are named in the library's existing convention:
John Singer Sargent 1856-1925, orDean Mitchell 1957-presentfor living painters, or the bare name when the years are not securely known. - When you sort a subfolder, it asks where the images should end up. The
default keeps painter folders inside the folder each image already lives
in (its top-level subfolder of the scanned root), so grouping folders
like
! RUSSIANSkeep their meaning. The alternative (r) empties a staging folder outward: everything sortable joins the painter folders at the library root — the right choice for an "unsorted" dump folder. - Only records whose attribution is secure (
givenorhighconfidence) move. Everything uncertain stays exactly where it is — those are thereview_queue.csvitems. - A file already sitting in its painter's folder is left alone; a file sitting in a different painter's folder is pulled out to the scanned root's folder for its real painter.
- Optionally, existing painter folders that lack years get them added
(
Anders Zorn→Anders Zorn 1860-1920); folders whose years disagree with the artist index are only flagged, never auto-renamed. Folder renames are journaled and undoable like everything else. - If two files would land in the folder with the same name, the newcomer gets
a
(2)suffix rather than overwriting. Emptied folders are left in place.
The repo is split into modules for development, but the Desktop workflow is one file you can copy anywhere:
python make_standalone.py
This writes dist/catalog_refs.py, a self-contained script that behaves
identically to running from the repo.
| File | Contents |
|---|---|
catalog_refs.py |
Entry point: settings, interactive menu, batch logic. |
record_schema.py |
The system prompt, the tool schema, and the movement canon. |
cleaning.py |
The repair pipeline every record passes through. |
artists.py |
The artist index: canonical names, years, bios, aliases. |
sorter.py |
The painter-folder sorter: plan, apply, undo. |
viewer.py + templates/wiki.html |
The offline wiki builder and its page template. |
make_standalone.py |
Inlines everything into dist/catalog_refs.py. |
These are deliberate. Changes should not break them.
- Cataloguing never moves, renames, or overwrites an image. All working
files go into
_databaseat the library root;wiki.htmlalone sits at the root itself, because its image links are relative and break anywhere else. Legacy root-level layouts are migrated automatically (migrate_layout). The sorter (sorter.py) is the single, explicit exception: it moves files only from a plan the user has seen, journals every operation tosort_undo.json, keeps the catalogue's paths in sync, and can undo the whole sort in one step. Nothing else may move a file. - Image paths are stored relative to the library root, and a run on a subfolder
grows the nearest existing library at or above it (
find_library_root), so the whole tree shares one master catalogue and one viewer. Moving a library (reposition_library) rewrites those relative paths so links keep resolving. wiki.htmlstays one self-contained file that works overfile://with zero network dependencies. It references images by relative path, which is why it must live in the library root.- Every record passes through the cleaning pipeline, both fresh from the API and when an existing catalogue is loaded. This auto-repairs old data. The order is fixed: strip leaked tool syntax, coerce tags to a list, normalise the movement onto the canonical list, recompute the review flag.
needs_reviewflags artist uncertainty only. A securely named artist with an unknown title or date is not review-worthy.- Artist-level facts live once in
artists.json, keyed by canonical name with an alias map for variants. Lookups only fill empty fields — a value the user edited by hand is never overwritten. Per-imagecontextstays about the work, never the painter's biography. MOVEMENT_CANONandMOVEMENT_INFOstay in sync: every canon value has an info entry with years, blurb, and wiki slug ("Other" has null years/wiki).- Batches are chunked at 1,000 requests / 200 MB, the in-flight batch id is
persisted to
_database/.catalog_state.jsonfor resume, and failed items are simply absent from the catalogue so a re-run retries them. max_tokens: 700and the 60-word caps in the prompt are cost controls.- Paths with
!, spaces, and backslashes are normal here. Test with them. - User data (catalogues, state, images) is never committed to this repo.