A local web app for culling large photo shoots. It scans a folder, generates cached thumbnails and previews for JPEG, camera RAW and HEIC, and puts every keep/reject/rate decision under the left hand so you never have to leave the keyboard.
The grid: lazy-loaded cached thumbnails, flags, ratings, and a live 'N to cull' counter.
Motivation
I came back from a trip with a few thousand photos, and the slow part was never the editing, it was deciding which shots were worth editing at all. Lightroom can do this, but it wants a catalog and an import step first, and it takes a lot of patience per frame. I wanted something that opens a folder instantly and keeps my hand on the home row until the folder is done. Marks live in a SQLite database keyed by file path, so nothing on disk gets touched until an explicit export step, and nothing leaves the machine.
Loupe view: scroll to zoom, drag to pan, and a non-destructive edit sidebar with a live histogram that overlays the original's tone curve.
Features
- One-handed culling: Shortcuts sit around the
ESDFnav cluster so the right hand stays free for the mouse.Wkeeps,Rrejects,1-5rates,Uundoes the last verdict, and flags auto-advance to the next photo - Loupe view with a non-destructive editor: Scroll to zoom, drag to pan, and a sidebar for rotate, straighten, flip, crop, auto-levels, white balance, border and tone. Sliders preview live using CSS/SVG filters that match the server's math, and the real render settles in once you let go. Edits go into a per-photo undo/redo stack, can be copied between photos, and saved as presets
- Compare and tournament voting: Select up to three near-identical frames and compare them with synchronized zoom and pan; one keystroke keeps the winner and rejects the rest. Select more than three and it becomes a king-of-the-hill playoff until one frame beats all the others
- RAW + JPEG pairing: A same-named RAW and JPEG collapse into one grid item, so marking, comparing, exporting and deleting always act on the whole pair
- Export that respects your edits: Copy or move keepers, rejects or rated shots into a folder, optionally preserving the subfolder structure and re-encoding JPEGs at a chosen quality. Edits are baked into the export, unedited files are copied byte-for-byte, and an edited RAW is never overwritten by a move
- Deliberate deletion: "Delete rejected" opens a review grid of every reject; click any to spare it, then delete only what's still highlighted, to the Recycle Bin where available
Compare mode: up to three frames with synced zoom and pan, zoom one and they all zoom. Press S/D/F to keep one and reject the rest.
Tech Stack
- Python, FastAPI and Uvicorn on the backend, served on localhost only
- Pillow for decoding and the edit pipeline,
rawpy/LibRaw for camera RAW,pillow-heiffor HEIC/HEIF - SQLite for marks, ratings and the edit stack, plus a disk cache of rendered thumbnails and previews pruned oldest-first
- Vanilla JavaScript on the frontend, no framework and no build step: one
app.js, one stylesheet,IntersectionObserverfor lazy thumbnails, canvas for the live histogram - Server-Sent Events for live export progress
Most of the engineering went into making a browser feel like a native culler: a two-tier render cache (a fast draft off a RAM-cached preview while a slider moves, a full-resolution render once it's released), neighbour preloading so navigation never waits, and a client-side edit preview whose math mirrors the server's closely enough that the picture doesn't jump when the real render lands.
Links
The source code is available on GitHub.