tripkit 1.1.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,298 @@
1
+ # TripKit Media Guide
2
+
3
+ Turn the photos and videos from a finished trip into an interactive map where every
4
+ stop shows what you actually captured there — and an optional, agent-assisted workflow
5
+ for captioning and fixing matches.
6
+
7
+ This guide covers:
8
+
9
+ 1. [What you get](#what-you-get)
10
+ 2. [Quick start](#quick-start)
11
+ 3. [How matching works](#how-matching-works)
12
+ 4. [The review file, field by field](#the-review-file-field-by-field)
13
+ 5. [Letting an AI agent do the review](#letting-an-ai-agent-do-the-review)
14
+ 6. [The `media` schema](#the-media-schema)
15
+ 7. [Using URLs instead of local files](#using-urls-instead-of-local-files)
16
+ 8. [Troubleshooting](#troubleshooting)
17
+ 9. [Command reference](#command-reference)
18
+
19
+ ---
20
+
21
+ ## What you get
22
+
23
+ Once media is attached to a trip, the rendered HTML gains:
24
+
25
+ - **Per-stop galleries** — a thumbnail strip and a `📷 N` count badge on each stop card and map popup.
26
+ - **A marker cue** — stops with media show a `📷 N` badge on their map marker; click it to jump straight to the photos.
27
+ - **A full-screen lightbox** — arrow keys / swipe / click to navigate, `Esc` to close, inline video playback, with each item's caption and capture time/GPS.
28
+ - **A photo-pin layer** — a toggleable map layer (the **📷 Photos** button, top-right) that drops a pin at the exact GPS of every geotagged item, separate from the day-numbered stop markers.
29
+
30
+ Stops without media render exactly as before — everything here is additive and optional.
31
+
32
+ ---
33
+
34
+ ## Quick start
35
+
36
+ You need: a finished `trip.yaml` (the same file the renderer already uses) and a folder of
37
+ photos/videos from the trip. Photos with EXIF GPS + timestamps match automatically; others
38
+ can be placed by hand (or by an agent) in the review step.
39
+
40
+ ```bash
41
+ # 1. Match your media folder to the trip's stops.
42
+ # Writes trip.media-review.yaml — nothing in trip.yaml changes yet.
43
+ npx tripkit media ./my-photos trip.yaml
44
+
45
+ # 2. Open trip.media-review.yaml. Add captions, fix any wrong matches,
46
+ # and place the items under "unmatched" that you want to keep.
47
+
48
+ # 3. Merge the reviewed media into the trip.
49
+ npx tripkit media apply trip.media-review.yaml trip.yaml
50
+
51
+ # 4. Re-render. The map now shows your photos.
52
+ npx tripkit trip.yaml trip.html
53
+ open trip.html
54
+ ```
55
+
56
+ > Running from a git clone instead of npx? Use `node convert.js …` in place of `npx tripkit …`.
57
+
58
+ **Thumbnails (optional).** If [`sharp`](https://www.npmjs.com/package/sharp) is installed,
59
+ step 1 also writes downscaled thumbnails to `my-photos/thumb/` and references them for fast
60
+ map/gallery loading. If `sharp` isn't installed, the step still works — it just uses the
61
+ full-resolution images directly. Install with `npm install sharp` to enable thumbnails.
62
+
63
+ ---
64
+
65
+ ## How matching works
66
+
67
+ `tripkit media` reads each file's EXIF metadata and assigns it to a stop using two signals:
68
+
69
+ 1. **Timestamp → day.** The photo's `DateTimeOriginal` is matched to the day whose `date`
70
+ falls on the same calendar date. (The year comes from `trip.dates`.) This keeps a photo
71
+ taken on Day 3 from matching a closer-looking stop on Day 5.
72
+ 2. **GPS → nearest stop.** Within that day, the item is assigned to the geographically
73
+ nearest stop (great-circle distance). The result is tagged with a `confidence`:
74
+ - `high` — within ~3 miles of the stop.
75
+ - `medium` — within ~25 miles.
76
+ - Anything farther, or with **no GPS**, or whose date matches **no day**, goes to the
77
+ `unmatched` bucket for you to handle.
78
+
79
+ Videos are routed to `unmatched` by default — most video formats don't expose GPS the way
80
+ photos do, so they're left for you to place rather than guessed at.
81
+
82
+ Nothing is destructive: matching only ever *proposes*. Your `trip.yaml` is untouched until
83
+ you run `media apply`.
84
+
85
+ ---
86
+
87
+ ## The review file, field by field
88
+
89
+ `tripkit media` writes `<trip>.media-review.yaml`. It's meant to be read and edited before
90
+ applying. A trimmed example:
91
+
92
+ ```yaml
93
+ trip_file: trip.yaml
94
+ summary:
95
+ total: 6 # files scanned
96
+ with_gps: 5 # had EXIF GPS
97
+ with_timestamp: 5 # had an EXIF capture time
98
+ matched: 4 # confidently assigned to a stop
99
+ unmatched: 2 # need your attention
100
+ thumbnails: generated # or: "skipped (sharp not installed)"
101
+
102
+ stops:
103
+ - day: 1 # ← which day (matches days[].number in trip.yaml)
104
+ stop_index: 0 # ← which stop within that day (0-based)
105
+ stop: Lower Yosemite Fall # name, for your reference (not used on apply)
106
+ media:
107
+ - src: media/fall_01.jpg
108
+ type: photo
109
+ caption: "" # ← WRITE THIS. Shown in the lightbox.
110
+ thumb: media/thumb/fall_01.jpg
111
+ lat: 37.7560 # original EXIF GPS (drives the photo-pin layer)
112
+ lng: -119.5965
113
+ taken_at: '2026-05-15T17:15:00Z'
114
+ confidence: high # advisory only; removed on apply
115
+
116
+ unmatched:
117
+ - src: media/random_sf.jpg
118
+ type: photo
119
+ caption: ""
120
+ lat: 37.7749
121
+ lng: -122.4194
122
+ taken_at: '2026-05-16T12:00:00Z'
123
+ reason: nearest stop "Glacier Point" is 156 mi away # why it wasn't auto-matched
124
+ suggested_stop: Glacier Point
125
+ - src: media/no_gps.jpg
126
+ type: photo
127
+ caption: ""
128
+ reason: no GPS in EXIF
129
+ ```
130
+
131
+ **What to edit:**
132
+
133
+ | Goal | Do this |
134
+ |------|---------|
135
+ | Add a caption | Fill in `caption:` (this is the highest-value edit — it's what the lightbox shows). |
136
+ | Move a photo to a different stop | Change its `day:` / `stop_index:` (under `stops:`), or move the item between entries. |
137
+ | Keep an unmatched item | Give it a `day:` and `stop_index:` — then it's applied like any matched item. |
138
+ | Drop an unmatched item | Leave it in `unmatched` without a `day`/`stop_index`. It's ignored on apply. |
139
+ | Remove a wrong match | Delete the item from the file before applying. |
140
+
141
+ **What's ignored on apply:** the `confidence`, `reason`, `suggested_stop`, and `stop` (name)
142
+ fields are review aids — they're stripped when merging into `trip.yaml`. Only
143
+ `src`, `type`, `thumb`, `caption`, `lat`, `lng`, and `taken_at` are written through.
144
+
145
+ **`media apply` is idempotent.** Re-running it won't duplicate items already present (it
146
+ matches on `src`), so it's safe to apply, render, tweak captions, and apply again.
147
+
148
+ ---
149
+
150
+ ## Letting an AI agent do the review
151
+
152
+ The review step — captioning every photo and placing the unmatched ones — is exactly the
153
+ kind of judgement work an AI agent is good at. The flow is designed so a human or an agent
154
+ can do it; the agent never touches your originals, only the review YAML.
155
+
156
+ A typical agent session:
157
+
158
+ > **You:** "I ran `tripkit media ./trip-photos yosemite.yaml`. Here's the review file —
159
+ > caption the photos and place the unmatched ones, then tell me the apply command."
160
+ >
161
+ > *(paste or share `yosemite.media-review.yaml`)*
162
+
163
+ The agent should:
164
+
165
+ 1. **Caption each matched item** — concise, specific, grounded in what the photo shows and
166
+ the stop it's at. "Lower Yosemite Fall roaring with May snowmelt," not "Photo 1."
167
+ It can lean on `stop`, `taken_at`, and the trip's own descriptions for context.
168
+ 2. **Resolve `unmatched` items** — use `suggested_stop`, the `reason`, the filename, the
169
+ `taken_at` time, and the trip itinerary to decide a `day` + `stop_index`, or leave
170
+ genuinely unplaceable items unmatched. A `reason: no GPS` selfie taken at 8pm on Day 2
171
+ probably belongs at that day's dinner or lodging stop.
172
+ 3. **Flag the suspicious ones** — if `suggested_stop` is 150 miles away, that photo likely
173
+ isn't from a planned stop at all; better to leave it out than force a wrong pin.
174
+ 4. **Hand back the edited review file** and the exact `tripkit media apply …` command.
175
+
176
+ If you use **Claude Code**, install the skill once and the agent already knows this flow
177
+ (it's Phase 5 of the TripKit agent skill):
178
+
179
+ ```bash
180
+ npx tripkit install-skill # → ~/.claude/skills/tripkit/
181
+ ```
182
+
183
+ Then: *"Use the tripkit skill to caption and place the media in this review file."*
184
+
185
+ **Why review-in-the-middle?** The agent works on a separate YAML, not your photo files or
186
+ your trip. You see every proposed caption and placement before anything is written, and the
187
+ `media apply` step is a plain mechanical merge you run yourself. The intelligence is
188
+ auditable, not a black box.
189
+
190
+ After the agent returns the file, validate and render:
191
+
192
+ ```bash
193
+ npx tripkit media apply yosemite.media-review.yaml yosemite.yaml
194
+ npx tripkit validate yosemite.yaml # surfaces e.g. media GPS far from its stop
195
+ npx tripkit yosemite.yaml yosemite.html
196
+ ```
197
+
198
+ ---
199
+
200
+ ## The `media` schema
201
+
202
+ Each stop (and, optionally, `lodging`) may carry a `media` array. It's purely additive to
203
+ the [existing schema](../schema/tripkit.schema.yaml).
204
+
205
+ ```yaml
206
+ stops:
207
+ - name: "Multnomah Falls"
208
+ lat: 45.576
209
+ lng: -122.116
210
+ image: "https://…" # unchanged: the single hero/primary image
211
+ media: # the gallery
212
+ - src: "media/IMG_2401.jpg" # relative path OR full URL (required)
213
+ type: photo # "photo" | "video"
214
+ thumb: "media/thumb/IMG_2401.jpg" # optional; renderer falls back to src
215
+ caption: "620-ft waterfall from the lower viewpoint" # optional
216
+ lat: 45.5762 # optional EXIF GPS; powers the photo-pin layer
217
+ lng: -122.1158
218
+ taken_at: "2026-04-07T14:32:00" # optional ISO 8601 capture time
219
+ ```
220
+
221
+ - `image` (the original single-image field) is untouched and still used as the hero/primary.
222
+ `media` is the new gallery shown in cards, popups, the lightbox, and the marker badge.
223
+ - Only `src` and a valid `type` are required per item; everything else is optional.
224
+ - Items that include `lat`/`lng` also appear as individual pins on the **📷 Photos** map layer.
225
+
226
+ `tripkit validate` checks every media item has a `src` and a valid `type`, and **warns**
227
+ when an item's GPS is more than ~25 miles from its stop — a strong hint it was placed on the
228
+ wrong stop.
229
+
230
+ ---
231
+
232
+ ## Using URLs instead of local files
233
+
234
+ `src` (and `thumb`) accept either a **relative path** or a **full URL**.
235
+
236
+ - **Relative paths** (the default from `tripkit media`) keep full-resolution media, but the
237
+ output is now an HTML file *plus* its media folder — ship them together.
238
+ - **URLs** (e.g. photos you've uploaded to S3, Cloudinary, Google Photos, or anywhere with a
239
+ direct link) keep the output a **single, self-contained, shareable HTML file** with no
240
+ media folder to carry around.
241
+
242
+ You can mix both in one trip. To use URLs, either point `src` at them when hand-editing the
243
+ review file, or swap the relative paths for URLs in `trip.yaml` after applying.
244
+
245
+ ---
246
+
247
+ ## Troubleshooting
248
+
249
+ **"Missing dependency exifr."** The ingest step needs it: `npm install exifr` (or just
250
+ `npm install` in a clone — it's a normal dependency). Only the `media` command needs it; the
251
+ renderer and `convert` don't.
252
+
253
+ **Thumbnails were skipped.** That's the `sharp`-not-installed path; it's fine. Run
254
+ `npm install sharp` to enable them. The review file's `summary.thumbnails` tells you which
255
+ path ran.
256
+
257
+ **Everything landed in `unmatched`.** Usually one of:
258
+ - The photos have no GPS (phone privacy settings, screenshots, exported/edited copies that
259
+ stripped EXIF). Place them by hand using `day` + `stop_index`.
260
+ - The photo dates don't line up with any day's `date`, so the day filter excludes every
261
+ stop. Check that `trip.dates` has the right year and each day's `date` is correct.
262
+
263
+ **A photo matched the wrong stop.** Edit its `day`/`stop_index` in the review file before
264
+ applying, or move it to the right `stops:` entry. If you already applied, fix it in
265
+ `trip.yaml` (the `media` array under that stop) and re-render.
266
+
267
+ **Videos didn't match.** Expected — they're routed to `unmatched` by default. Give them a
268
+ `day` + `stop_index` to place them; they play inline in the lightbox.
269
+
270
+ **The marker badge / photo-pin layer didn't appear.** The badge only shows on stops that
271
+ have `media`; the **📷 Photos** toggle only appears when at least one media item has its own
272
+ `lat`/`lng`. If your items lack GPS, the galleries and lightbox still work — just not the pins.
273
+
274
+ ---
275
+
276
+ ## Command reference
277
+
278
+ ```text
279
+ tripkit media <folder> <trip.yaml>
280
+ Scan <folder> for photos/videos, read EXIF GPS + timestamps, auto-match each
281
+ to the nearest stop on the matching day, optionally generate thumbnails, and
282
+ write <trip>.media-review.yaml. Does not modify <trip.yaml>.
283
+
284
+ tripkit media apply <review.yaml> <trip.yaml>
285
+ Merge the reviewed media into <trip.yaml>'s stops[].media[]. Idempotent.
286
+
287
+ tripkit validate <trip.yaml>
288
+ Validate the trip, including media (src present, type valid, GPS sanity).
289
+
290
+ tripkit <trip.yaml> [output.html]
291
+ Render to interactive HTML (auto-validates first).
292
+ ```
293
+
294
+ Supported media extensions: photos — `.jpg .jpeg .png .heic .heif .webp .tif .tiff`;
295
+ videos — `.mp4 .mov .m4v .avi .mkv .webm`.
296
+
297
+ See also: the [README](../README.md) for the project overview and the
298
+ [agent skill](../agent/SKILL.md) (Phase 5) for the agent-facing version of this flow.
package/docs/README.md ADDED
@@ -0,0 +1 @@
1
+ Screenshot placeholder — replace with actual screenshot of the TripKit visualizer