Files
photoanalyzer/WEBAPP_CONCEPT.md

18 KiB
Raw Permalink Blame History

Photo Analyzer — Web App UX Concept

Transform photo_analyzer.py from a terminal pipeline into a local web app that reuses the nsfwtag review-app design language and architecture. Concept first — implementation follows after review.

North star: the analyzed data (descriptions, tags, mood, people, year, …) is currently invisible — it lives in a SQLite DB nobody can see. The web app's primary job is to surface it (browse + FTS5 search); the analysis pipeline becomes a monitored background job instead of a foreground terminal takeover.

Design validated with the ui-ux-pro-max skill: Data-Dense Dashboard pattern (KPI cards, status colors green/amber/red, filtering-first, hover tooltips, row highlight), debounced search with a real "no results" state, list virtualization for 25k+ photos, and — to stay dependency-free — CSS progress bars with values-always- visible for stats rather than a chart library (the skill's AAA-accessible option).


1. Architecture (reuse nsfwtag's, unchanged in spirit)

  • One stdlib ThreadingHTTPServer on 127.0.0.1:<random-free-port>, opened in the browser. No framework, no build step, no external assets (matches nsfwtag).
  • One token-injected HTML page (analyzer.html, %%TOKEN%% replacement) + JSON endpoints. Same "real HTML file, not a Python string" rule.
  • SQLite stays the single source of truth. The server never owns pipeline state; it reads the DB and an in-memory run-state object (exactly like the terminal _Dashboard reads dicts + a deque). A render/HTTP crash can never corrupt the DB.
  • Path args validated against the DB on every request (nsfwtag validates against the scanned candidate set) — the server can't be pointed at arbitrary files.
  • The existing analysis engine (_run_folder_loop, workers, retry/backoff, _db_lock, variant grouping, nsfw-skip, _IGNORE exclusion) is reused verbatim, driven from a background thread. The web layer is a new view + control surface, not a rewrite.

Endpoints

Method Route Purpose
GET / The app page
GET /img?path= Thumbnail bytes (reuse nsfwtag)
GET /exif?path= Raw EXIF + decimal GPS + Maps URL (reuse nsfwtag)
GET /photo?path= Full analyzed record for one photo (all DB fields)
GET /search?q=&setting=&tod=&season=&people=&year_min=&year_max=&status=&album=&sort=&offset= Paged result set (FTS5 when q present) → {rows:[…], total, offset}
GET /facets Distinct filter values + counts (for the toolbar)
GET /stats Aggregates for the Stats view
GET /progress Live run state: {running, totals, tokens, eta, albums:[…], feed:[…]}
POST /run Start analysis with a config body (the CLI flags)
POST /stop Signal the current run to stop (reuses the existing _stop event)
GET /balance · /quota-check Proxy the existing functions → JSON
GET /log Activity log (reuse nsfwtag)

Live updates use polling (/progress ~1.5 s, /log ~2 s) — same pattern as nsfwtag's log panel. No SSE/WebSocket dependency.


2. Information architecture

Flat, three top-level views behind a header segmented control (deep-linked via #library / #analyze / #stats; active state highlighted per the skill's nav-state-active). An Activity-log slide-in panel is available from any view.

Header (sticky):  [◧ logo]  Photo Analyzer            ⟨ Library · Analyze · Stats ⟩        [ total 25,318 · analyzed 25,316 · errors 8 ]   [☰ log]
                                                          ^ segmented, active = --text on --elev
  • Library (default) — browse + search the analyzed photos. The payoff.
  • Analyze — run controls + the live dashboard (album progress, feed, tokens/ETA).
  • Stats — aggregates, breakdowns, error list.

┌ Header ─ view: Library ──────────────────────────────────────────────────────────┐
├ Toolbar (sticky) ─────────────────────────────────────────────────────────────────┤
│ 🔍 Search description, tags, mood, place…   Setting▾  Time▾  Season▾  People▾       │
│ Year 1998 ●──────● 2019   ☐ has location   Status▾        Sort: Relevance▾          │
├──────────────┬────────────────────────────────────────────────────────────────────┤
│ Folders      │  ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐   ← virtualized / infinite  │
│ ▸ Urlaub     │  │ [img] │ │ [img] │ │ [img] │ │ [img] │     scroll, server-paged    │
│   2015 USA   │  │ '15 👤2│ │ '04   │ │ '11 👤5│ │  ⚠err │                            │
│ ▸ job        │  └───────┘ └───────┘ └───────┘ └───────┘                             │
│ ▸ selfies    │   beach sunset…  cathedral…   wedding…    (error: timeout)            │
│   …          │  … 25,316 results …                                                  │
└──────────────┴────────────────────────────────────────────────────────────────────┘
  • Search (.field): debounced 250 ms → /search FTS5 over description, tags, mood, location_hint. Autocomplete-style live results (skill Search/Autocomplete). Empty query = browse-all (paged). No-results state shows a message + suggestions ("try a broader term, or clear filters"), never a blank grid.
  • Facet filters map every analyzed field:
    • Setting (indoor/outdoor) · Time of day · Season → select or .chip toggles.
    • People → buckets 0 / 1 / 2 / 3+.
    • Year → dual-handle range slider (approx_year minmax), tabular numbers.
    • Has-location → toggle (location_hint not null).
    • Status → analyzed / exif_written / error / pending.
    • Filters AND with the search; each updates the live result count.
  • Folder sidebar (reuse nsfwtag .sidebar/.fitem): album = leaf folder (reuse album_label), with counts; filter to a folder + its subfolders.
  • Card (reuse .card/.thumb): lazy /img thumbnail; overlays — year badge
    • people badge (reuse .score badge styling), a status dot (error=red, pending=faint), and the first line of the description under the thumbnail. Tag chips on hover.
  • Grid scale: 25k+ photos → server-paged infinite scroll (append on scroll), not load-all (skill virtualize-lists, content-jumping). Reserve card height to avoid layout shift.
  • Lightbox (reuse nsfwtag lightbox + keyboard + prev/next within the current result set): left = full image; right = a two-section panel:
    1. Analysis — description, tags, mood, setting, time-of-day, season, people, year, location. (The EXIF caption Immich will index, made human-readable.)
    2. EXIF — raw fields + GPS Google Maps link (reuse /exif). Per-photo action: Re-analyze this one (POST /run scoped to one path).

4. View: Analyze (run controls + live dashboard)

Mirrors the terminal _Dashboard (see DASHBOARD_PLAN.md) as a web surface.

┌ Header ─ view: Analyze ───────────────────────────────────────────────────────────┐
├ Run bar ──────────────────────────────────────────────────────────────────────────┤
│ Library [ /…/pictures ▾ ]   ☐ Dry-run ☐ Re-analyze ☐ No-EXIF ☐ EXIF-only ☐ Variants │
│                                            [ Balance ] [ Quota check ]   [▶ Start]   │
├ Overall ──────────────────────────────────────────────────────────────────────────┤
│ ████████████████░░░░░░░░  238 / 550  43 %      ok 235 · err 3                        │
│ tokens 284,192 · est. remaining ~340,000 · ETA 14:23              [■ Stop] (danger)  │
├ Albums (data-dense) ──────────────────┬ Recent feed ───────────────────────────────┤
│ Camera Roll     238/550 ███████░░ 43% │ ✓ IMG_4821.jpg                              │
│ Urlaub/Rom       45/ 45 ████████ ✓    │     A golden retriever on a sunny beach     │
│ Urlaub/Venedig   12/ 60 ██░░░░░░ 20%  │ ✗ DSC_0042.jpg — 4 retries: timeout         │
│ WhatsApp Images   0/120 ░░░░░░░░  0%  │ ✓ IMG_4820.jpg                              │
│ …                                     │     Two children building a sandcastle      │
└───────────────────────────────────────┴─────────────────────────────────────────────┘
  • Run bar = the CLI flags as controls: Library path (default from LIBRARY env) · Dry-run · Re-analyze · No-EXIF · EXIF-only · Group-variants toggles · Start (primary) / Stop (danger). One run at a time (guarded); Start on an interrupted library simply resumes (DB is truth) — resume-safety is automatic, no separate control.
  • Overall panel: big progress bar, ok/err counts, tokens used + estimated remaining + ETA (all tabular). Status colors: green done, amber running, red error.
  • Albums panel: one row per leaf-folder album with a mini progress bar + when fully done (the DASHBOARD_PLAN target, now clickable → jumps to that album in Library). Future albums shown at 0 %.
  • Recent feed: newest-first list — thumbnail + filename + one-line description; errors in red with the message. Reuses the activity-log line styling.
  • Balance / Quota-check: small buttons → /balance, /quota-check → toast/inline result. Debug log: available via the Activity-log panel.
  • Everything here is driven by polling /progress; the panel degrades to "Idle — no run in progress" with a Start hint when nothing is running (empty state with action).

5. View: Stats (aggregates + errors, no chart lib)

┌ KPI cards ────────────────────────────────────────────────────────────────────────┐
│  25,318 total   25,316 analyzed   25,316 EXIF-written   8 errors   2 pending         │
├ Breakdowns (CSS bars, counts always visible) ───────────────────────────────────────┤
│ Setting   outdoor ██████████████ 14,902   indoor ████████ 9,120                     │
│ Season    summer ████████ · autumn ██████ · spring ████ · winter ███ · unknown ██   │
│ People    0 ████████ · 1 ██████ · 2 ████ · 3+ ███                                   │
│ By year   1998▁ 2003▃ 2004▅ 2010▇ 2011█ 2015█ …            (histogram of bars)       │
│ Top tags  beach 1,204 · wedding 980 · city 900 · food 610 …                         │
├ Errors ─────────────────────────────────────────────────────────────────────────────┤
│ DSC_0042.jpg  — connection timeout (4 retries)                       [Retry]         │
│ …                                                                                    │
└──────────────────────────────────────────────────────────────────────────────────────┘
  • KPI cards reuse .metric. Breakdowns are horizontal CSS bars with the count printed (skill: values-always-visible, AAA; no dependency, no CSP issue).
  • Error table lists status='error' rows with error_message + a Retry action (re-queues that path for the next run — matches "re-running retries error records").

6. Component inventory (reuse existing tokens)

Everything below already exists in nsfwtag/review.html's :root token set and class library — the web app inherits it wholesale (same dark OLED palette, radii, focus rings, reduced-motion guard).

Component Source New/changed
Header .app/.brand/.logo/.titles reuse title copy
KPI .metrics/.metric reuse used for header counts + Stats cards
View switcher reuse .seg active-state = current view
Toolbar .toolbar reuse
Search .field reuse
Facet .chip / select / .seg reuse People/Setting/Season/Status
Range slider input[type=range] reuse dual-handle year range (small JS)
Sidebar .sidebar/.fitem reuse album tree
Grid .grid/.card/.thumb reuse + year/people badges, status dot
Tag chips reuse .chip (mini)
Lightbox + EXIF panel reuse + Analysis section
Buttons .btn/.primary/.danger/.ghost reuse Start/Stop/Balance
Toast .toast reuse
Activity log panel reuse run events too
Progress bar new .bar (fill = --primary) overall + per-album + stat bars
Run toggles new switch styled from tokens the 5 flags

One new token: --warn:#f59e0b (amber) for "running / partial" states — the only gap in the current green/red/faint status set.


7. Interaction model

  • View switching: header segmented control; state deep-linked in the URL hash and preserved on back/forward (skill deep-linking, state-preservation).
  • Search + filters: debounced; combine as AND; the URL hash encodes q + active filters so a search is shareable and survives reload. Result count updates live.
  • Browse at scale: infinite scroll appends server-paged results; card heights reserved (no CLS); thumbnails lazy-load.
  • Lightbox: ←/→ navigate the current result set, Esc/backdrop close; EXIF and full record fetched lazily on open.
  • Analyze: Start → POST /run (config) → server spawns the run on a background thread → dashboard polls /progress. Stop → POST /stop → sets the existing _stop event; in-flight requests drain, DB stays consistent, restart resumes.
  • Accessibility/motion: keyboard nav + visible focus rings (inherited), status never by color alone (badges carry text/icons), prefers-reduced-motion respected (token media query already present), all touch targets ≥ 3644 px.

8. CLI → Web feature parity

CLI feature Web surface Preserved how
--library Analyze Run bar path (default LIBRARY env) same discovery
--dry-run Run toggle passed to engine
--reanalyze Run toggle passed to engine
--no-exif Run toggle passed to engine
--exif-only Run toggle passed to engine
--group-variants Run toggle reuses variant grouping
--stats Stats view /stats aggregates
--balance Balance button /balance
--quota-check Quota-check button /quota-check
--debug Activity-log panel server log
resume-safety automatic (Start resumes) DB source of truth
_IGNORE/ exclusion server-side discovery discover_photos unchanged
nsfw-skip automatic in every run filter_nsfw_tagged unchanged
FTS5 search (CLI had none) Library search new surface for existing FTS5 table
DB (all fields) Library cards + lightbox + facets new surface for existing schema

Nothing is dropped. The only genuinely new capability is surfacing what the DB already stores (search/browse/stats) — everything else is the same engine behind a web control panel instead of a terminal.


9. Proposed file layout (mirrors nsfwtag/)

webapp/                     (or analyzer_web/)
├── __main__.py     # CLI: python -m webapp  → starts server, opens browser
├── server.py       # ThreadingHTTPServer + routes (reuse nsfwtag/server.py shape)
├── page.py         # render analyzer.html with %%TOKENS%% (reuse webapp.py shape)
├── analyzer.html   # the single-page frontend (inherits nsfwtag tokens/classes)
├── query.py        # DB read helpers: search (FTS5), facets, stats, one-photo
└── runner.py       # drive photo_analyzer's engine on a background thread + run-state

photo_analyzer.py stays the pipeline library; runner.py imports and drives it. No duplication of analysis logic.


10. Build order (for after approval)

  1. Read-only Library first — server + page + /search + /facets + grid + lightbox over the existing DB. Immediately useful, zero risk to the pipeline.
  2. Stats view/stats + CSS breakdown bars.
  3. Analyze view/run + /stop + /progress polling + run-state object, driving the existing engine on a thread.
  4. Balance/quota/log wiring + polish (empty states, deep-link hash, reduced-motion).

Each step ships independently; the pipeline keeps working from the CLI throughout.