Skip to content

krites — studio interface contract (web UI + HTTP/SSE API)

Status: DRAFT / intent (companion to 0001-krites.md and 0002-interface-contracts.md) Owner: Matt Cockayne Last updated: 2026-06-21

0. How to read this

This is the authoritative interface contract for the studio — krites' primary surface (0001 §10). It defines, before any studio code is written, the studio's functional requirements (R-UI-*), its UX, and the HTTP + SSE API contract (R-API-*) the frontend talks to. It supersedes the studio summary in 0002 §4 (which now points here).

  • The CLI command contracts (0002 §2–§3) and the engine remain the source of truth; the studio is a richer driver of the same operations on the same shoot workspace. Anything the studio does maps to a CLI/engine operation (R-UI-PARITY).
  • Requirement IDs (R-AREA-n) give traceability: each maps to ≥1 test (Go API test, godog scenario, or a frontend test). Stable handles, not a priority order.
  • Keywords: MUST = a contract a test enforces; SHOULD = expected but negotiable; MAY = optional / later phase.
  • Phase tags: [P1] ships with the first studio; [P2]/[P3] are the develop / object-removal phases — their endpoints are specified now so the interface anticipates them, but are not built yet.
  • The frontend framework is an implementation detail behind this API. The contract is framework-agnostic. The working default is Svelte (+ Vite, Svelte stores for state, TanStack Virtual / svelte-virtual for the grid) — chosen for the smallest footprint (compiles to vanilla JS, no virtual-DOM runtime), the cleanest go:embed, and Wails-readiness (0001 §13-Q2). Vue/Solid/Preact would honour the same contract; nothing here depends on the choice.

1. Purpose & scope

krites studio [--port N] starts a local, single-user, localhost-bound web server (GTB service lifecycle; frontend go:embed-ed into the one binary) and opens a browser UI. It is desktop-first — culling thousands of frames is a big-screen, keyboard-driven job (the inverse of keryx's phone-first studio).

Phase-1 studio scope [P1] — everything the current engine already supports: shoot library, the cull-review grid + loupe (verdicts, ratings, reasons, burst compare), running/refreshing the cull with progress, export keepers, and XMP write, plus a cull-profile settings panel.

Later [P2]/[P3] — the develop canvas (straighten/crop/look), retouch, and object-removal brush. Specified in §5 so the API shape is stable, built later.

2. Surfaces & levels

Three levels, library-first:

  1. Library (home) — the shoots krites knows about; open one to drill in.
  2. Cull review (the heart) — the grid + loupe over one shoot's frames.
  3. Finish — Export and XMP actions (and, [P2], the develop canvas).

3. Functional requirements

3.1 Library & shoots

  • R-UI-1 (MUST) [P1] The studio opens on a shoot library: a list of known shoots, each showing name, date, frame count, the verdict tally (keep / maybe / reject), and the cull profile in use. The CLI scopes to one shoot; switching between many is a studio capability (R-SCOPE-2).
  • R-UI-2 (MUST) [P1] Register a shoot — pick a folder to ingest (krites ingest), with an optional name; it joins the library.
  • R-UI-3 (MUST) [P1] Rename a shoot and remove it from the library (remove forgets the shoot; it never deletes the user's files, R-ND-1).
  • R-UI-4 (SHOULD) [P1] The library persists across sessions in a studio-level store (the known-shoot paths), distinct from any single shoot's .krites.

3.2 Cull review (the heart)

  • R-UI-10 (MUST) [P1] A virtualised grid + a loupe over a shoot's frames, performant on 4,000+ frames — preview-backed, never loading full originals into the grid (R-UI-PERF).
  • R-UI-11 (MUST) [P1] Run / refresh the cull from the UI, with live progress (frames analysed of total) over SSE; on completion the grid shows verdicts + reasons. Re-running respects the analysis cache when present.
  • R-UI-12 (MUST) [P1] Set a frame's verdict (keep / maybe / reject) and a 1–5 star rating by keyboard and click; an override is recorded as a human decision (R-VRD-1) and persists to verdicts.yaml.
  • R-UI-13 (MUST) [P1] Keyboard-first review (Lightroom muscle memory): P keep · X reject · U maybe · 15 rating · ` clear · arrows navigate · Space toggles loupe. The map is shown in a help overlay (?).
  • R-UI-14 (MUST) [P1] Filter by verdict and by reason, and show a per-frame reasons overlay ("why rejected") sourced from verdicts.yaml.
  • R-UI-15 (MUST) [P1] Near-duplicate bursts are shown grouped, the kept (sharpest) frame marked, with a side-by-side compare to change which frame is kept (re-tags the burst, R-DUP-3).
  • R-UI-16 (MUST) [P1] Multi-select + bulk actions. A selection model — click, shift-click range, ⌘/Ctrl-click toggle, and select-all (in the current filter) — drives a contextual action bar that sets one verdict/rating across the selection in a single step (e.g. "reject the 1,722 filtered rejects", "keep all", "reject a burst's losers"). Each bulk change is one undoable step (R-UI-UNDO) and persists via the batch endpoint (R-API-9). (Gap found while mocking up §4.2.)
  • R-UI-17 (MUST) [P1] Every studio mutation writes the same records the CLI does (verdicts.yaml); there is no studio-only state for a shoot (R-UI-PARITY).
  • R-UI-UNDO (MUST) [P1] Undo / redo of verdict/rating changes (single and bulk) is available and total (R-ND-3), with a visible toolbar affordance and ⌘/Ctrl-Z · ⌘/Ctrl-⇧-Z. It is client-managed history that replays verdict writes through the verdict/batch endpoints — no dedicated undo endpoint. (Gap found while mocking up: undo had no UI home.)

3.3 Develop, retouch & remove (later phases)

  • R-UI-20 (MUST) [P2] Develop panel: straighten/crop with on-canvas handles and a live before/after; a look picker; retouch toggles.
  • R-UI-21 (MUST) [P3] Object removal: brush/box a region → preview the inpaint → accept/reject; the privacy indicator (R-UI-PRIV) flags a cloud-backed inpainter.

3.4 Export & XMP

  • R-UI-30 (MUST) [P1] Export panel: a verdict-set selector (keep / maybe / reject, multi-select; default keep) shows the frame count for the chosen set, then runs krites export into export/ with progress and a result summary. (Gap found while mocking up: the finish bar only offered keepers.)
  • R-UI-31 (MUST) [P1] Write XMP action: run krites xmp write to drop Lightroom-readable sidecars beside the originals; the panel states the verdict→rating/label mapping and that originals are untouched.

3.5 Settings & privacy

  • R-UI-40 (MUST) [P1] Cull-profile settings: view/edit the active profile's thresholds + dedup distance; saving re-resolves verdicts without re-analysis (R-CAT-2) and writes through the GTB config layer (hot-reload).
  • R-UI-41 (MUST) [P2] Looks and providers settings (when develop / pluggable backends land). Secrets are never written to config (R-PRIV-3).
  • R-UI-PRIV (MUST) [P1] A persistent privacy indicator shows whether any active provider is cloud-backed and which operation would send data off-machine (R-PRIV-2). With only local backends it reads "local-only".

4. UX

4.1 Visual language

Minimalist and modern over the krites brand: a neutral light base (near-white panels, soft slate text, hairline borders) with aubergine-plum and champagne-gold as restrained accents only — active states / primary actions / selection (plum), and the keep/award highlights (gold). The frame thumbnails are the colour in the room; the chrome stays quiet. Dark mode is a nice-to-have.

4.2 Layout (desktop-first)

 Library (home)                      Cull review (one shoot)
┌──────────────────────────────┐   ┌──────────────────────────────────────────────┐
│ krites · Shoots      [+ open] │   │ ‹shoots  Smith Wedding ▾   ⟳cull  ⬚filter  ⚙ │
│ ┌──────────────────────────┐ │   ├───────────────────────────────────┬──────────┤
│ │ Smith Wedding            │ │   │ GRID (virtualised thumbnails)     │  LOUPE   │
│ │ 21 Jun · 3814 frames     │ │   │ [▣][▣][▣][▣][▣][▣][▣][▣]          │ ┌──────┐ │
│ │ keep 612 · maybe 1480 …  │ │   │ [▣][▣][◳][▣][▣][⛌][▣][▣]  ◳=burst  │ │ frame│ │
│ ├──────────────────────────┤ │   │ [⛌][▣][▣][★5][▣][▣][▣][▣]          │ └──────┘ │
│ │ Patel Wedding  ○ unculled│ │   │  filter: ▸reject ▸blur ▸blinks    │ P X U ★  │
│ │  open  rename  remove   ⋮│ │   ├───────────────────────────────────┴──────────┤
│ └──────────────────────────┘ │   │ Export ▸ keep  ·  Write XMP  ·  why: motion… │
└──────────────────────────────┘   └──────────────────────────────────────────────┘
   Burst compare (overlay): the cluster's frames side by side, the sharpest marked
   ★, click another to keep it instead.
  • Wide screens: grid + collapsible loupe. Narrower: the loupe overlays.
  • R-UI-PERF (MUST) [P1] The grid is memory-bounded: only visible rows render (virtualised), thumbnails are server-sized preview JPEGs (never full originals), off-screen images are evicted — verified to stay flat on 4,000 frames (0001 §13-Q2).

4.3 UX principles

  • R-UI-PARITY (MUST) The studio exposes nothing the CLI/engine can't do on the workspace; UI and CLI are interchangeable on the same shoot.
  • R-UI-INSTANT (SHOULD) Verdict/rating keystrokes feel instant — applied optimistically in the client, persisted async; a failed write surfaces and rolls back.
  • R-UI-A11Y (SHOULD) Review is fully keyboard-operable; the keyboard map has a discoverable help overlay (?).

5. HTTP + SSE API contract

The frontend talks to a thin krites HTTP API over the shoot workspace (GTB pkg/http). Endpoints map to engine/CLI operations; both are tested against the same workspace assertions. JSON in/out; previews are image bytes; long operations stream via SSE. Versioned under /api/v1.

Method · path Engine / CLI analogue Phase Notes
GET /healthz (GTB) P1 liveness
GET /api/v1/shoots library store P1 known shoots + counts + profile (R-UI-1)
POST /api/v1/shoots ingest P1 {path, name?} → register a shoot (R-UI-2)
POST /api/v1/shoots/{id}/rename (library) P1 {name} (R-UI-3) — /rename sub-path (Go ServeMux has no {id}:verb form)
DELETE /api/v1/shoots/{id} (library) P1 forget shoot; never deletes files (R-UI-3)
GET /api/v1/shoots/{id} manifest P1 name, date, counts, profile/look
GET /api/v1/shoots/{id}/frames verdicts.yaml P1 paged frame list: verdict, rating, reasons, cluster, preview URLs (R-UI-10)
GET /api/v1/shoots/{id}/frames/{frame}/preview?size=thumb\|loupe preview cache P1 server-sized JPEG; never the full original (R-UI-PERF)
POST /api/v1/shoots/{id}/cull (SSE) cull P1 runs/refreshes; streams progress events, then done (R-UI-11)
PUT /api/v1/shoots/{id}/frames/{frame}/verdict verdict override P1 {verdict?, rating?} → writes verdicts.yaml as a human override (R-UI-12, R-VRD-1)
PUT /api/v1/shoots/{id}/verdicts bulk override P1 {frames:[...], verdict?, rating?} → one batch write for multi-select / bulk actions (R-UI-16, R-API-9)
GET /api/v1/shoots/{id}/clusters dedup result P1 bursts for the compare view (R-UI-15)
POST /api/v1/shoots/{id}/clusters/{cid}/keep re-pick best P1 {frame} → keep this frame, demote siblings (R-DUP-3)
POST /api/v1/shoots/{id}/export export P1 {verdicts:[keep…]} (default [keep]) → render the chosen set into export/ (R-UI-30)
POST /api/v1/shoots/{id}/xmp xmp write P1 write sidecars beside originals (R-UI-31)
GET·PUT /api/v1/shoots/{id}/profile per-shoot profile.yaml P1 thresholds + dedup distance; PUT saves and re-resolves verdicts preserving human overrides (R-UI-40, R-CAT-2, R-VRD-1). Re-runs analysis for now; a signal cache (re-resolve without re-decode) is the follow-up
GET /api/v1/providers provider config P1 active backends + whether cloud (privacy indicator, R-UI-PRIV)
PUT /api/v1/shoots/{id}/frames/{frame}/develop develop settings P2 straighten/crop/look (R-UI-20)
POST /api/v1/shoots/{id}/frames/{frame}/remove Inpainter P3 {mask} → inpaint preview (R-UI-21)

API requirements:

  • R-API-1 (MUST) The server binds localhost by default and is not exposed publicly without explicit opt-in + the GTB HTTP auth (0001 §10.2).
  • R-API-2 (MUST) Non-destructive by construction: no endpoint mutates an original. Verdict/rating writes go to verdicts.yaml; export writes only under export/; xmp writes only .xmp companions (R-ND-1/2, R-EXP-2, R-XMP-3).
  • R-API-3 (MUST) The frames/preview endpoint serves server-sized preview JPEGs, never streams full originals to the grid (R-UI-PERF).
  • R-API-4 (MUST) The cull endpoint streams progress over SSE and is cancellable — closing the stream cancels the run (R-GLOBAL interruptible); partial progress already persisted is not lost.
  • R-API-5 (MUST) A verdict PUT is idempotent and records the change as a human override; concurrent edits last-writer-wins on a single frame (single user, so no locking — 0001 §10).
  • R-API-6 (MUST) Validation: a malformed body returns 422 with field-level errors before any write (mirrors R-GLOBAL-4).
  • R-API-7 (MUST) When an active provider is cloud-backed, responses that would trigger off-machine calls carry a disclosure flag the UI surfaces (R-UI-PRIV, R-PRIV-2).
  • R-API-8 (SHOULD) The API is the only coupling between frontend and backend, so it is independently testable with curl/Go tests and the frontend is swappable (§0).
  • R-API-9 (MUST) The bulk verdict endpoint applies one verdict/rating to many frames in a single workspace write (not N round-trips), records them as human overrides, and returns the updated counts so the client can reconcile its optimistic state (R-UI-16).

6. Non-functional contracts (recap)

  • Local, single-user, localhost-bound (R-API-1).
  • Non-destructive end to end (R-API-2).
  • Memory-bounded grid via virtualisation + server-sized previews (R-UI-PERF, R-API-3).
  • CLI/engine parity — the studio adds reach, not new domain logic (R-UI-PARITY).
  • Renderer-agnostic & Wails-ready — a browser today, a native macOS app later wrapping the same SPA, no contract change (0001 §13-Q2).

7. Open questions

  1. Library store location.RESOLVED: ~/.krites/shoots.yaml — the studio's known-shoot list (paths + display names), distinct from any single shoot's .krites, surviving across sessions (R-UI-4). Revisit if a hosted variant needs per-user stores.
  2. Preview generation timing.RESOLVED: lazy with a background warm — ingest stays fast; previews are generated on first grid view and cached under .krites/previews/, with a background pass warming the rest (R-UI-PERF).
  3. Cull trigger.RESOLVED: auto-run on first open of an unculled shoot, with a visible, cancellable progress toast; a manual ⟳ Cull refreshes (respecting the analysis cache) thereafter (R-UI-11).
  4. Bulk-action granularity.RESOLVED in R-UI-16: [P1] ships click / shift-range / ⌘-toggle / select-all-in-filter selection + a one-step verdict/rating bulk apply via the batch endpoint (R-API-9).

(All four were closed after the §4.2 mockup pass validated the workflows.)