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-virtualfor the grid) — chosen for the smallest footprint (compiles to vanilla JS, no virtual-DOM runtime), the cleanestgo: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:
- Library (home) — the shoots krites knows about; open one to drill in.
- Cull review (the heart) — the grid + loupe over one shoot's frames.
- 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 toverdicts.yaml.R-UI-13(MUST)[P1]Keyboard-first review (Lightroom muscle memory):Pkeep ·Xreject ·Umaybe ·1–5rating ·`clear · arrows navigate ·Spacetoggles 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 fromverdicts.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 runskrites exportintoexport/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: runkrites xmp writeto 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 toverdicts.yaml; export writes only underexport/; xmp writes only.xmpcompanions (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-GLOBALinterruptible); 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 returns422with field-level errors before any write (mirrorsR-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 withcurl/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¶
- 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. - 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). - 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). - 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.)