Files
postcard/PROPOSAL.md
T
Fredrik JohanssonandClaude Sonnet 5 cf0f5fca2c Initial commit: postcard v1
Phone photo -> composited postcard (canvas templates, stamp corner,
EXIF-aware date/location, self-hosted handwriting font) with a small
server for share links, day-context caption drafting off goonk's
day-summary API, and Nominatim reverse geocoding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 19:49:03 +02:00

144 lines
6.6 KiB
Markdown

# Proposal: `postcard` — turn a phone photo into a sendable postcard
**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
Upload a photo from your phone (or any device), it gets composited onto
a vintage postcard template — border, stamp corner, a handwritten-style
caption — and you get a shareable link or a downloadable image. The
caption is auto-drafted from whatever context is available for that
day (git activity, what was playing, a note title) so the postcard reads
like it was actually written *from* that day, not just a filter applied
to a photo.
Structurally this is `latent`'s upload → transform → deliver shape, with
the transform swapped from film-emulation filters to postcard
compositing, and one addition: a short generated caption line instead of
(or alongside) canvas filters. Reuse `latent`'s validated pieces
wholesale rather than re-deriving them — same phone-upload flow, same
blob+SQLite+TTL server, same no-accounts/link-is-the-credential model.
## Architecture
Static/client-heavy frontend (canvas compositing, same "no build step"
family as `latent`/`typo`/`flit`/`wisp`) plus a small single-process
server, copied from `latent/server` almost unchanged: `db.js` (SQLite
metadata), `ids.js` (opaque random IDs), `cleanup.js` (interval TTL
sweep), `ratelimit.js` (basic IP throttle on upload), `routes.js`,
`index.js`. No accounts, no auth — the link is the credential, same as
`latent`/`wisp`.
### Editing (client-side, `<canvas>`)
1. **Upload** — `<input type="file" capture>` for direct camera access,
normal file picker as fallback (same open question `latent` already
flagged, same answer: ship both).
2. **Template** — a handful of postcard border/stamp-corner presets
(plain cream border, torn-edge, faded color-photo-era border) — pick
a vibe, not a full editor. Reuse `latent`'s "presets, not sliders"
call for the same reason: faster to ship, more toy than tool.
3. **Caption** — an editable text field, pre-filled by a generated draft
(see below), rendered onto the card in a handwriting-style webfont.
Editable because auto-drafted text should be a starting point, not
the final word — the person sending it should be able to make it
actually theirs.
4. **Postmark/stamp corner** — today's date and a small generated
"location" stamp if EXIF GPS is present (round-tripped through the
same reverse-geocoding approach `rewind`/`galr` already use, if one
exists — otherwise just the date, no invented location).
5. Compositing is canvas-based, non-destructive within the session
(recompute from the original photo + a param object, matching
`latent`'s pattern) so switching templates doesn't require
re-uploading.
### Caption generation
The interesting new piece `latent` didn't need. Draft a one-line caption
from whatever of these is available and non-empty for the photo's
date (EXIF date if present, else upload date):
- Git activity that day (via the same Gitea-API aggregate-count approach
`rewind` already built — "spent the day untangling a deploy script" /
"quiet one, no commits")
- What was playing (Spotify plays log, same source `/wrapped`/`/now`
already read — "[artist] on repeat")
- A note/blog title from that date, if one exists
This is explicitly a **remix of existing fragments**, not a generative
free-text model call — same spirit as the haiku/found-poetry idea
below: assemble from real fragments of that day, phrase them into one
short sentence with a small set of template shapes ("Spent the day
{git_fragment}. {spotify_fragment} the whole time." /
"{spotify_fragment}. {git_fragment}, mostly."), and always leave it
editable. If none of the three sources have data for that date, fall
back to a small set of generic postcard-voice lines ("Wish you were
here. Mostly wasn't.") rather than leaving the field empty.
### Finishing paths
Same two-path split as `latent`:
1. **Instant** — composite, then download directly, entirely
client-side, no server touched. Optional "generate share link"
(explicit action, not automatic).
2. **Send** — uploads the composited image to server storage, returns a
link. No delayed-reveal mechanic here (that's `latent`'s gimmick, not
this one) — the postcard metaphor's own delay is real mail taking a
few days to arrive, which the link doesn't need to fake.
### Server
Copied from `latent/server`'s shape almost directly:
**Table `postcards`:**
| column | type | notes |
|---|---|---|
| `id` | text PK | random opaque ID, used in the URL |
| `blob_path` | text | composited image on disk |
| `mime` | text | |
| `created_at` | integer | |
| `expires_at` | integer | TTL sweep target, same as `latent` |
**Endpoints:**
- `POST /api/upload` — body: composited image bytes. Returns `{ id, url }`.
- `GET /p/:id` — serves the postcard image directly (no develop-later
gate — this app's whole point is immediacy, unlike `latent`).
- TTL sweep, same interval-based pattern as `latent`/`wisp`.
No auth. Rate-limit `/api/upload` the same way `latent` does.
## Non-goals
- No accounts, no "your postcards" history — every card stands alone via
its own link, same throwaway philosophy as `latent`/`wisp`.
- No delayed-reveal mechanic — that's `latent`'s gimmick; this one's
point is speed, not anticipation.
- No free-text LLM-generated captions — template-and-fragment assembly
from real personal data only, kept editable, never presented as if the
app "wrote" it.
- No collaborative/multi-recipient cards — one photo, one card, one link.
## Open questions
- **Where the day-context lookups live** — `rewind` and `/wrapped`/`/now`
already have working Gitea-API and Spotify-log integrations; decide
whether `postcard`'s server calls those APIs directly (network
dependency on goonk's API being reachable) or whether this only makes
sense as a goonk-hosted feature rather than a fully separate sibling
project. Worth resolving before writing `routes.js`.
- **Location stamp source** — confirm whether `galr`/`rewind` already do
reverse geocoding anywhere reusable, or whether this ships without a
location stamp in a first pass.
- **Handwriting webfont choice and licensing** — pick one, check its
license allows self-hosting.
- **Image size limits** — same question `latent` flagged, same likely
answer (downscale on upload, cap stored size).
- **Retention window** — shorter than `latent`'s, probably — a postcard
link is meant to be opened once by the recipient, not browsed later.