3,478 shots, 13 edited
One of the twelve. August 2025, Nikon Z8, edited in Lightroom. The other 3,466 frames from that safari are still RAW.
There's an SSD on my desk with 13,590 files on it, 12,752 of which are Nikon Z8 RAWs from guest safaris at the lodge I run in the Maasai Mara. One folder, a single guest safari from August last year, holds 3,478 of them. Inside it is a folder called processed with 13 JPEGs, two of them from the same frame, which is the honest count of what I've ever finished from that safari: twelve frames. The other folders on the drive look the same, and there are other drives.
In May I wrote about indexing a year of video on a 2021 MacBook with a local model, and in that Hacker News thread I described this exact problem in a comment on my own post: the question for any trip is which 150 of the 1,500 shots are worth processing at all. That comment became the work queue.
What changed since May
framedex started as a video indexer: every clip gets a plain-text .description.md sidecar next to it with GPS, a transcript, faces, and a keep/review/cull rating from a vision model, on the bet that a text file next to the original outlives any app. Photos joined in June, the trust work and the Lightroom export landed in July, then the repo sat for two months while KaribuKit, the lodge software that pays for all this, had the summer. Last night it got the rest.
| When | What | The one line |
|---|---|---|
| July | fdx-xmp |
keep/review/cull becomes 3★/2★/1★ + Red in Lightroom via .xmp sidecars, never touching originals |
| Sept 9 | Burst grouping | Frames shot within 2 s on one camera, and RAW+JPEG pairs, share one assessment |
| Sept 9 | fdx-mcp |
The archive as tools for Claude Desktop, LM Studio, any MCP host; a contact sheet the model can look at; one write, user_rating |
| Sept 9 | v0.2.0 | First tag on the repo |
The last three rows are one night's work, and I didn't write the code. I described the two features to Claude Code, set OpenAI's Codex as the reviewer, and went to bed. By morning Codex had 38 findings on the burst PR and 37 on the MCP one, among them a filename with a newline in it that could smuggle a write flag into the exiftool argument stream. I merged them at 10:34 and 10:36, once the 334 tests were green and both had run on real files.
One vision call per burst, and what 16 zebra frames cost
Sixteen files copied off the SSD, none touched in place. The old pipeline would've made 16 vision calls. The new one made four.
For the burst, framedex renders each RAW's embedded preview, scores it with the variance of the Laplacian (a sharpness number computed locally, without a model), and sends only the sharpest frame to the model. The eight scores came back between 832.5 and 853.6: eight nearly identical frames, one of them very slightly less soft. I assumed that's what every burst looks like. It isn't, and the folder proved it. Frame 6 got the vision call and a full sidecar. The other seven got stub sidecars that copy its assessment, carry their own EXIF and score, and say in plain text that the model never saw them. The whole group block on a stub is six lines:
group:
kind: burst
id: b_afa797ed
primary: false
primary_file: _DSC0006.NEF
sharpness: 832.8
The three RAW+JPEG pairs were also two seconds apart, so the pairing and the burst rule composed, and six files became one call. The exact rule is in the README.
| Group | Files | Vision calls |
|---|---|---|
| Zebra burst, 8 frames within 2 s | 8 | 1 |
| Three RAW+JPEG pairs, also within 2 s | 6 | 1 |
| Two lone frames | 2 | 2 |
| Total | 16 | 4 |
The burst, rendered by the new contact-sheet tool from the RAWs' embedded previews. Cell 6 is the frame the sharpness score picked. At this size I can't tell it from 3, and the score separated them by 19 points out of 850, which is not a verdict.
Those four calls went through the Claude CLI, so four preview JPEGs left the machine. --backend local against LM Studio keeps them on it, and that's how the big run will go.
Then the whole folder, without the model
Grouping needs EXIF, not a model, so I pointed framedex's grouping code at all 3,478 NEFs on the SSD, read-only, and then scored every burst frame's embedded preview the way the pipeline does. The exiftool pass took 53 seconds. Scoring 2,886 previews took eight minutes on the laptop.
| Frames | 3,478 over three game-drive days |
| Bursts (3 or more frames, gaps of 2 s or less) | 385, covering 2,886 frames |
| Singles | 592 |
| Vision calls for the folder | 977, down from 3,478 |
| Longest chains | 103, 62, 60 frames |
Left: how far the softest frame in each burst sits below the sharpest, across all 385 bursts. Right: burst sizes. 121 bursts are three frames long; 20 run past 20.
The zebra burst was the easy case. Across the 385 bursts, the gap between the sharpest and softest frame was 27% of the sharpest at the median. Only 48 bursts were under 5%, and the zebra burst, at 2.3%, sits among the twelve tightest in the folder.
Twenty of the bursts run to 20 frames or more, up to 103, and inside those the median spread is 64%, so the chains are too long. A 103-frame chain isn't one moment, it's a Z8 at 17 frames a second with pauses under two seconds, chained across re-frames and subjects, and one verdict copied onto 103 stubs is the wrong shape. The next change to the grouping is a cap on chain length or duration; until then --no-group exists.
Eight of the 103 frames in the longest chain, a cheetah chase in tall grass. The gaps never reached two seconds, so framedex called it one burst and would have copied one verdict onto all 103.
Eleven of my twelve edited frames came from bursts, and the sharpness score would have picked the same frame as me once. In the 62-frame chain my two picks ranked 45th and 55th of 62 for sharpness; in a 32-frame chain, 29th of 32. Sharpness is a fine way to choose which frame gets the vision call and no way to choose the photograph, which is why the primary gets 3★ and the sheet goes to a human.
From the 62-frame chain, embedded camera previews, unedited. Left and middle are the two frames I edited. Right is the frame the sharpness score would have sent to the model: 334 against my 201 and 215, and neither cub is looking at the camera.
Getting the ratings into Lightroom Classic
fdx-xmp wrote a .xmp next to each copied RAW, tagged burst-primary on frame 6 and burst-alternate on the rest. Then I opened Lightroom Classic with an empty test catalog and let Claude Code drive it through the accessibility API: import the 13 NEFs, then Metadata → Read Metadata from Files. The grid came up with frame 6 at 3★ and its seven alternates at 3★ with it, which is the ceiling, the model never gives 4 or 5; frame 1, which I'd rated cull through the tools before the import, came up at 1★ with a red label, and the caption on frame 6 was the model's scene sentence.
The part I wanted to see work is the loop back. With the catalog open, frame 3 was rated cull from the chat side, fdx-xmp regenerated its sidecar, and Read Metadata from Files flipped it in the grid from 3★ to 1★ with a red label. A rating decided in a chat window is a filter in the editor within a minute, and nothing in Lightroom's own develop settings was touched, because framedex only ever overwrites an .xmp whose bytes match the ones it wrote.
Lightroom already has a culling panel
Adobe's own answer sits in the same Library panel. Lightroom Classic now ships an Assisted Culling panel with Subject Focus, Eye Focus and Eyes Open filters and face overlays, and it can stack a burst on its own. For culling one shoot inside Lightroom it's the right answer for most people. On my zebra catalog it reports no faces found, which is fair, they're zebras.
Three things it doesn't do. Nothing it decides survives outside the catalog, so there's no text next to the file to grep in 2040. People on r/Lightroom complain that you can't re-run it on a set without reimporting the RAWs, where a sidecar is regenerated with one command. And no agent can ask it anything. For a per-shoot workflow inside Lightroom, use Adobe's. This is for an archive that spans drives and includes video.
Talking to the drive over MCP
fdx-mcp is the piece I was least sure about. Six tools over stdio, and the two that matter are a contact sheet of up to 20 files the model can look at, and one write, a user rating. The host supplies the model, so the same tools work from Claude Desktop and from LM Studio with a local vision model, and framedex itself makes no model call. A contact sheet is one image per request, which is the entire cost model. The tool list and the setup for each host are in docs/mcp.md.
It's a server and not a chatbot because I couldn't think of anything a framedex chat window would do that Claude Desktop or LM Studio don't already do better. What none of them have is the index in plain text next to the files, and that's the part worth owning.
What the model can and can't judge in a photo
Whether the code works is the boring half. What I want to know is whether a model can make the artistic call, one to five stars, so the review becomes a quick confirmation. Technical rejects the model gets right nearly every time: missed focus, motion blur, blown exposure, closed eyes, the frame you took by accident. Near duplicates within a burst it gets right by construction. What it can't do is the 4★ versus 5★ call, because each frame is judged alone with no memory of my taste and no consistency across a set, and its idea of strong composition is stock-photo composition. That's the reason for the 3★ ceiling, and I'm not raising it on a hunch. The top two stars stay mine.
Moving the top end doesn't need a better model, because models compare far better than they score: a pass that shows the sheet for each group and asks which two frames are strongest should beat the sharpness pick, at one call per group instead of per frame, and it could hardly do worse than one in eleven. And I have a Lightroom catalog with years of my own stars in it, which is a labelled dataset I've been ignoring: a hundred photos I've already rated, run through framedex, tell me within an afternoon how far the model is from my taste and which of its verdicts to trust. That experiment comes before any more features.
The next post is the run itself: 977 vision calls over the August folder on a local model. I'll report how many frames it culled, how many of its keeps I disagreed with, whether it flagged any of my twelve, and how long the review took against the twelve I managed by hand. If the agreement is bad, that's the post.
Code and the 0.2.0 release are at github.com/Simbastack-hq/framedex. If you run it on a folder of your own RAWs, open an issue with the sharpness spread inside your bursts. Mine is 27% at the median, and I'd like to know whether a Z8 on a game drive is typical.
— NJ
I run Mara Hilltop, the lodge in the Maasai Mara these photos come from, and build KaribuKit, the hotel platform it runs on, at SimbaStack.