webapp — Photo Analyzer web UI
A local, browser-based UI for the photo_analyzer.py pipeline. Browse and
full-text-search the analyzed library, watch analysis runs live, and start/stop
runs — reusing the nsfwtag review-app design (dark OLED, stdlib HTTP server, one
token-injected HTML page). Nothing leaves the machine.
python -m webapp # DB + library default from photo_analyzer.env
python -m webapp --db photo_analysis.db --library pictures/
python -m webapp --no-open # don't auto-open a browser
See ../WEBAPP_CONCEPT.md for the full UX concept and rationale.
Views
- Library — FTS5 search over description/tags/mood/location + facet filters (setting, time of day, season, people, year range, has-place, status), a folder sidebar (album = leaf folder), an infinite-scroll grid, and a lightbox with an Analysis panel (all DB fields) and a raw EXIF panel (incl. Google Maps link for geotagged photos).
- Analyze — run controls mapping every CLI flag (dry-run / reanalyze / no-exif /
exif-only / group-variants), a live overall progress bar, per-album progress, and a
recent feed. Start/Stop drive
photo_analyzer.pyas a subprocess; Stop is the analyzer's own SIGINT drain. Balance / quota-check buttons. A Duplicates & maintenance row runs the content-hash actions (backfill hashes / find duplicates / mark duplicates) — no API calls; output streams to the Activity log and marked duplicates appear under theduplicatestatus filter. (Move/rename reconciliation and perceptual hashing also run automatically at the start of every normal run.) - Stats — KPI cards + CSS breakdown bars (setting/time/season/people, by-year histogram, top tags) + an error list. No chart library.
Architecture
server.py— stdlibThreadingHTTPServeron127.0.0.1:<free-port>; routes below.query.py— read-only DB access (FTS5 search, facets, stats, one-photo).runner.py— drivesphoto_analyzer.pyas a subprocess; DB-derived progress.page.py/analyzer.html— the single frontend page (inherits nsfwtag's tokens).- SQLite stays the source of truth; the server only reads it (+ a
busy_timeoutso a concurrent analysis write never errors a read)./imgand/exifpaths are validated against the DB — the server can't be pointed at arbitrary files.
| Route | Purpose |
|---|---|
GET / |
the page |
GET /search?q=&setting=&tod=&season=&people=&year_min=&year_max=&has_location=&status=&album=&sort=&offset= |
paged results (FTS5 when q) |
GET /facets · /stats |
filter options · aggregates |
GET /photo?path= · /exif?path= · /img?path= |
one record · raw EXIF · thumbnail |
GET /progress · /log |
live run state · activity log |
POST /run · /stop |
start / stop a run — body {library, ...flags} or {library, action} where action ∈ backfill-phash | list-dupes | dedupe |
GET /balance · /quota-check |
provider balance · one-shot quota probe |
Known ceilings (ponytail)
/imgserves full-resolution originals, not resized thumbnails — a 120-card page can move a lot of bytes. Fine locally; add a Pillow thumbnail+cache endpoint if the grid feels slow. (Same limitation as nsfwtag.)- HEIC files won't render in the browser (broken thumb); metadata/search still work.
- The Analyze tokens/ETA readout is omitted — those live only in the terminal dashboard's memory; the subprocess model surfaces DB-derived progress instead. Parse run stdout for token counts if you want them back.