How I rebuilt finescenery.com on an AI pipeline

PhotoMind: a Python pipeline that turns 20 years of Lightroom into a fast static site

Two months ago I rebuilt finescenery.com, my landscape photography portfolio, on a different foundation. The new site is generated end to end from a Python pipeline I call PhotoMind. The same archive, the same Lightroom catalog, but now the curating happens where I want it to happen - in Lightroom - and the site catches up automatically.

This is the build story. I wrote the original on finescenery.com itself for visitors there; this is the same engineering content reframed for anyone interested in what an AI-augmented end-to-end build looks like in practice.

The starting mess

The starting state is the one most long-running personal archives end up in: 117,166 files spread across half a dozen drives, ranging from RAW captures off a Nikon D700 in 2008 to last month's mirrorless ARWs. The Lightroom catalog has rough star ratings and roughly 26,500 keyword assignments. The previous version of the portfolio site was hand-picked images with hand-typed captions; it never grew past a few hundred pieces because updating it was a chore.

117,166
Files indexed
~26,500
Keyword assignments
5,789
Winners exported

I wanted a site that:

  • pulled directly from the Lightroom catalog, so curating in Lightroom curated the site
  • generated titles, alt-text, and descriptions automatically with quality good enough to ship
  • grouped photos by where and what (country, region, place, scene, mood) without me hand-tagging each one
  • stayed fast: static pages, no CMS, no database calls at request time

That is what PhotoMind became.

How it works

PhotoMind is a Python project that owns one SQLite database (~268 MB) and writes content into the Astro repo for finescenery.com. Three layers, roughly.

01

Indexing

Scans every photo on disk. Extracts EXIF via ExifTool, parses Lightroom XMP sidecars, computes CLIP embeddings via Hugging Face Transformers on PyTorch. Links each file 1:1 to the Lightroom catalog. The output is the SQLite ground truth.

61,277 CLIP vectors
02

Understanding

Runs vision LLMs over the Lightroom-rendered JPEGs to generate descriptions, alt-text, scene tags, mood, composition, and genre. Three providers: Qwen-VL at two sizes (running locally on a personal GPU) plus Claude. Stored per-piece-per-provider so I can compare.

~8,400 vision passes
03

Site export

Selects the portfolio. Deduplicates burst frames. Maps every piece to a canonical URL. Encodes responsive AVIF and WebP at five sizes via Pillow and pillow-avif-plugin. Writes Astro markdown. Astro builds it; Cloudflare Pages serves it globally.

2,001 static pages

The interesting parts

A few of the design choices are unusual enough to be worth calling out.

Lightroom is the source of truth

Every winner image on the site is a JPEG I personally exported from Lightroom at rating 1+. PhotoMind links each of those back to its source RAW via basename and EXIF matching. Curating in Lightroom is curating the site. There is no double-bookkeeping between catalog and site, and no admin panel to maintain. 5,789 winners, 100% mapped.

Bucket-batched captions

Letting a vision LLM generate captions one by one produces formulaic slop - every other title becomes "Golden Light Over the Ocean". Photos are grouped instead into buckets by (narrative, place, subregion, region, country, year-month) of up to ten. Claude Sonnet sees the whole bucket at once with a prompt that bans formula leading words and demands variety.

Output goes into a site_metadata table keyed by (piece_id, prompt_version), making re-runs idempotent. Two tiers in the same pass: a short description for meta tags and RSS, a longer one for the body of the piece page.

Gazetteer-driven URLs

Lightroom keywords are messy: "Lazovsky", "Lazovsky Reserve", "lazovsky reserve", "Lazo". A hand-written gazetteer resolves them all to canonical (kind, slug) tags at every hierarchy tier (country, region, subregion, place). The geographic tree builds itself from that. Tail countries with under 10 pieces collapse into /world/. Sub-galleries with under five pieces collapse into the parent.

Three theme axes

Alongside the geographic tree, three taxonomies sit in parallel: feature (30 entries), composition (13), mood (19). Hand-curated lists drawn from raw vision-tag frequencies across the corpus. The same tokenizer pattern feeds the raw vision output in and gets back the canonical site tags. That is how /mood/golden-hour/ and /feature/storm-clouds/ exist without me ever sitting down to tag photos by hand.

Client-side CLIP search

Every piece has a 512-dim CLIP embedding. When you type "misty forest" or "sea stacks at dawn" into the search box, the query is encoded in the browser via a small precomputed text encoder and ranked by cosine similarity over those vectors. There is no server doing the matching, just static vector files and a few hundred lines of browser JavaScript. The same CLIP model that powered tag propagation now powers search at zero runtime cost.

A few representative pieces from the live site

Layered coastal rocks at Coffs Harbor catch diffuse light from a storm-clouded sky, with long-exposed ocean waves appearing as smooth white mist.Rippled wet sand at Spoon Bay reflects intense orange dawn light, with breaking surf and a hazy horizon visible in the distance.A bare-branched coastal tree on the left stands against granite boulders and open ocean water under a clear sky at Bay of Fires, Tasmania.Rust-coloured lichen-covered rocks in the foreground, lit by low-angle golden light, against a distant headland and deep blue ocean, Tasmania.

What I learned

Four things stood out by the end.

01. AI captioning works, but bucket-batching is the difference

Per-piece prompts produce templated copy. Per-bucket prompts with explicit variety constraints produce something I would ship. The cost is the same.

02. Local vision LLMs already understand your photos

You just need to ask nicely. Qwen-VL running on a personal GPU described 5,500+ images before I called a single hosted API. A clear prompt and a downscaled image are most of the work. Hosted models still win on style and variety, but the local ones do the heavy lifting for free.

03. Lightroom-as-source-of-truth is liberating

Once it is set up, I just rate pictures in Lightroom and the site catches up on the next export run. Zero copy-paste between catalog and site.

04. Static is faster than everything else

The whole site is plain HTML, CSS, and AVIF. Cloudflare serves it for free. There is no admin panel because there is nothing to admin.

Read more on finescenery.com

This writeup is the condensed version. The originals on finescenery.com go a bit deeper, particularly on the photography side.

Published: May 1, 2026