# Proposal: `latent` — a phone-photo editor with a "develop it later" option **Status:** proposed, not built. Self-contained — written so a fresh agent with no prior conversation context can pick this up and implement it without anything explained first. No repo exists yet; this file is the seed of one. ## Motivation A small, fun webapp: upload a photo from your phone, apply film-style manipulation (grain, light leaks, vignette, halation, color-cast) in the browser, and either download it immediately or — the silly, on-brand part — send it off to "develop," which holds it server-side behind a randomized delay (hours to days) before the link actually resolves to the image. Getting your photos back later, like real film, on purpose, for no practical reason except that it's a fun mechanic. Named `latent` after the *latent image* — the real photographic term for the picture that already exists on exposed film but is invisible until developed. Both halves of this app map onto that word directly. Two independent finishing paths off one editor, not two separate apps: 1. **Instant** — edit, then download directly, entirely client-side, no server touched. Optionally generate a share link for the edited image (explicit user action — "generate share link" — not automatic). 2. **Delayed** — edit, then "send to develop": uploads the result to server storage, assigns a `develop_at` time chosen randomly within a configured range, and gives back a link that won't resolve to the actual image until that time passes. ## Architecture Static/client-heavy frontend (canvas-based editing, same "no build step, plain HTML/CSS/JS" family as `typo`/`flit`/`wisp`'s frontend) plus a small single-process server for the two paths that need persistence: share links and delayed development. Reuses the shape already validated by `wisp` — blob + SQLite metadata + a background TTL-style sweep — rather than inventing a new storage pattern. No accounts, no auth: the link itself is the credential, same as `wisp`. Unlike `wisp`, this does **not** need end-to-end encryption — none of this is sensitive, and the entire point is a link other people can open. That actually makes the backend simpler than `wisp` despite doing more: no client-side crypto, no key-in-fragment handling, just an opaque random ID per stored image. ### Editing (client-side, no server involved until "generate link" or "send to develop" is pressed) All via `` — pixel manipulation, no ML, no WASM needed for a first pass: - **Grain** — random noise overlay, opacity/size adjustable - **Light leaks** — a handful of pre-baked semi-transparent PNG overlays (warm streaks/blooms near an edge), randomized placement/rotation per application so repeat use doesn't look identical - **Vignette** — radial darken toward the edges - **Halation** — bloom/glow around bright areas (blur a thresholded bright-pixel mask, screen-blend back over the original) - **Color cast / white balance shift** — a few presets (warm/expired-film amber, cool/cross-processed teal, faded/low-contrast) rather than raw sliders for every channel — keep the UI to "pick a vibe," not a full Lightroom clone - Adjustments should be non-destructive within a session (recompute from the original + a param object, not chained lossy operations) so dragging a slider back to zero actually gets you back to the original ### Server Single process, SQLite for metadata (same pattern as `galr`/`stash`), images as blobs on local disk (same pattern as `wisp`). **Table `images`:** | column | type | notes | |---|---|---| | `id` | text PK | random opaque ID, used in the URL | | `blob_path` | text | where the file lives on disk | | `mime` | text | | | `created_at` | integer | | | `develop_at` | integer, nullable | null = instant share (no delay gate); set = "send to develop" path | | `expires_at` | integer | TTL sweep target — don't keep images forever regardless of path | **Endpoints:** - `POST /api/upload` — body: image bytes + `{ delayed: boolean }`. If `delayed`, server picks `develop_at` randomly within a configured range (e.g. env-configurable `DEVELOP_MIN_HOURS` / `DEVELOP_MAX_HOURS`) and returns the link immediately — the whole point is you get the link *before* it resolves, so you can send it to someone and both of you wait. If not delayed, `develop_at` is null and the link resolves right away (that's the "optional share link" path off instant download). - `GET /i/:id` — serves the image if `develop_at` is null or already passed; otherwise a small "still developing, check back in ~Nh" page (server can compute and show a rough remaining time without revealing the exact `develop_at`, to keep a little mystery — exact behavior is an open question below). - TTL sweep (interval-based, same "deliberately dumb, not push-based" precedent as `keep watch`/`wisp`'s sweep) deletes blobs + rows past `expires_at` regardless of whether they were ever viewed. No auth on any endpoint — this is a public, no-accounts toy. Rate-limit `/api/upload` (basic IP-based throttle) to avoid it becoming an open file host. ## Open questions - **Exact delay range** — "hours to days" is the pitch; needs an actual default (e.g. 2–48 hours?) and whether it's configurable per-upload or fixed server-side via env. - **What the "still developing" page shows** — exact countdown vs. vague "come back later" vs. a tiny animated placeholder (fogged/blank photo silhouette) for flavor. Worth spending a *little* design effort here since it's the whole gimmick. - **Filter presets vs. raw controls** — start with presets-only (faster to ship, more "fun toy" than "editing tool") and add fine controls later if it turns out sliders are actually wanted. - **Mobile upload flow specifics** — `` for direct camera access vs. just a normal file picker; probably both, file picker as the fallback. - **Image size limits / re-encoding** — phone camera photos can be large; decide whether to downscale/re-encode on upload (bandwidth + storage) or store as-is up to some cap. - **Retention for the instant/non-delayed path** — does a same-length TTL apply, or does it expire faster than the delayed path since it's a much more casual "just give me a link" flow? ## Non-goals - No accounts, no galleries, no "your uploads" history — every image stands alone via its own link, consistent with `wisp`'s throwaway philosophy. - No e2e encryption — deliberately out of scope, see Architecture. - No collaborative/multi-user editing — one uploader, one result.