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.
- package/CHANGELOG.md +48 -5
- package/README.md +34 -3
- package/agent/SKILL.md +168 -0
- package/convert.js +63 -0
- package/docs/MEDIA-GUIDE.md +298 -0
- package/docs/README.md +1 -0
- package/examples/new-england-fall-2026.yaml +471 -0
- package/examples/nyc-long-weekend-2026.yaml +283 -0
- package/examples/oregon-spring-2026.yaml +17 -0
- package/examples/southwest-parks-2026.yaml +463 -0
- package/media-ingest.js +389 -0
- package/package.json +14 -3
- package/renderers/html/tripkit-renderer.html +341 -33
- package/schema/tripkit.schema.yaml +25 -0
- package/scripts/check-skill-coverage.js +66 -0
- package/scripts/test-media-lifecycle.js +190 -0
- package/validate.js +97 -0
- package/agent/AGENT-SKILL.md +0 -105
|
@@ -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
|