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>
6.6 KiB
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>)
- Upload —
<input type="file" capture>for direct camera access, normal file picker as fallback (same open questionlatentalready flagged, same answer: ship both). - 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. - 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.
- 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/galralready use, if one exists — otherwise just the date, no invented location). - 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
rewindalready built — "spent the day untangling a deploy script" / "quiet one, no commits") - What was playing (Spotify plays log, same source
/wrapped//nowalready 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:
- Instant — composite, then download directly, entirely client-side, no server touched. Optional "generate share link" (explicit action, not automatic).
- 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, unlikelatent).- 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 —
rewindand/wrapped//nowalready have working Gitea-API and Spotify-log integrations; decide whetherpostcard'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 writingroutes.js. - Location stamp source — confirm whether
galr/rewindalready 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
latentflagged, 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.