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>
This commit is contained in:
+143
@@ -0,0 +1,143 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user