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>
144 lines
6.6 KiB
Markdown
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.
|