Or as my wife says: How I managed to turn 3 hours of boring GoPro footage into 30 seconds of boring GoPro footage
Road cycling has been my primary form of exercise, social outlet, therapy, wardrobe expense, and personality trait for almost a decade. I recently moved from Boston to New York, and my biggest anxiety was losing my 6:15am friend group that lapped to Dover or Blue Hills before work (WhatsApp group title: Boston WattsApp). Fortunately, the NYC cycling scene is awesome. I’ve made and rekindled many friendships, I have a new WattsApp group that’s great for Central Park laps before work, longer rides up 9W on the weekends, and the occasional Bear Mountain summit or 130+ mile ride to Montauk. There is no better way to start a day than 50 miles with friends and a cold brew + maple bacon log from Boxer Donut in Nyack.
Like many casual cyclists, I would do (or spend) anything to get faster at cycling, other than change my lifestyle in any way. I have all the tech and all the numbers: spandex, aero bike, carbon wheels, head unit, power meter, three separate heart rate monitors (Whoop, Garmin Watch, chest strap). Earlier this year I added a handlebar-mounted GoPro to the mix.
Of course, absolutely no one wants to watch 3 hours of being stuck behind Citibikes on West Side Highway. But I enjoy stitching together my favorite sections. I look through past footage, identify the fun parts, and put together a narrative to remember (remember, Strava is my personality). But since cycling is already time consuming, manually editing a highlight reel edit per ride was a nonstarter. So I built and open-sourced a pipeline called ride-recap to do it automatically.
Three months, 25 rides, 45 commits, and 1000 miles later, I can take hours of cycling footage plus a .fit file (time-series data with GPS, speed, heart rate, cadence, power, etc.) and turn it into a finished highlight reel. Every single second of the ride is scanned by gemini-3.5-flash, as is the clip ranking + curation. The whole thing costs about $0.04 per ride and takes 10 minutes. Here’s a finalized highlight:
Architecture
The system has six steps and one command: ride-recap process data/raw/<YYYY-MM-DD>/
- Ingest: pull GoPro data (
.mp4+.LRV) and Garmin.fitfile into a ride folder - Detect: four candidate sources identify interesting parts of the ride
- Gemini vision: a two-pass frame scan for visually compelling segments (more on this later)
- Telemetry: speed, climbs + descents, power + heart rate spikes
- Strava API: popular segments on the route (e.g. State Line Sprint has 343 stars), this is the pavement every NYC cyclist knows
- (Optional) Manual Labels: Streamlit app, occasionally I’ll hand-flag a moment as a “must include” (say, if I see a deer)
- Fuse: Generate candidate clips, rank them, penalize nearby moments (more on this later)
- (Optional) Review: Streamlit app, if I want to override the candidate ranking or rate clips
- Compose: Burn a ride telemetry overlay onto the selected clips
- Output:
highlight_landscape.mp4(60s, 16:9) for YouTube,highlight_portrait.mp4(30s, 9:16) for Instagram
Here’s a pre/post for the step five burned overlay: you can see I’ve added live telemetry on the left, a GTA style mini-map + route trace on the right, and elevation profile + ride progress along the bottom.
What Makes Cycling Visuals Compelling?
Many have opined on the value of Taste™ in the post-AI era. Four months ago, I would have answered like Justice Potter Stewart describing pornography: I know it when I see it. I’ve now processed 180,430 seconds + frames through this pipeline. Here’s my strongest opinion on teaching taste to a LLM: taste develops through iteration count, you cannot specify taste in advance.
I started by sending every frame to Gemini with a prompt to “rate visual interest 1-5”. But a ride around New York could be interesting for so many reasons! A concrete jungle view from Hudson Yards or a Hudson River view from Peekskill, a climb up River Road or a descent down Bear Mountain, almost getting dropped in a paceline on 9W or almost getting hit by an e-bike going the wrong way on 9th Avenue.
Here’s an example of what I have in place now: every second of every ride goes through the same versioned prompt. I send Gemini six 848×480 proxy frames plus one line of telemetry, and I get one JSON object back. Flip through three calls from the July 12th ride below: a sprint through Hudson Yards, a beautiful view from under the GW Bridge, and two seconds stuck behind a black SUV. Any taste I developed came from me banging my head against JSON outputs I disagreed with.
The best way to improve a prompt is to fix its mistakes. That requires ground truth, so I created a Streamlit app and manually labeled a Central Park ride. I reviewed the entire ride frame-by-frame alongside telemetry graphs, and ranked around 30 moments I found interesting or boring, plus a description of why.
- Gemini was impressed by any power output above 200W. I’m no Tadej Pogacar, but 200w isn’t that impressive.
-
Solution: athlete specific FTP/power zones injected into the system prompt. 200W means nothing without knowing who’s pushing the pedals.
power_zones = ( f"Athlete power zones (FTP={ftp}W):\n" f" Z1 Active Recovery <{int(ftp * 0.55)}W | Z2 Endurance <{int(ftp * 0.75)}W | " f"Z3 Tempo <{int(ftp * 0.90)}W\n" f" Z4 Threshold <{int(ftp * 1.05)}W | Z5 VO2max <{int(ftp * 1.20)}W | " f"Z6 Anaerobic <{int(ftp * 1.50)}W | Z7 Neuromuscular {int(ftp * 1.50)}W+\n" "Only Z5+ qualifies as genuinely hard effort. Z3-Z4 is moderate. Z1-Z2 is easy." )
-
- Gemini loved clips of tunnels, and being stuck behind cars
-
Solution: negative examples — a real intersection from the April 14th ride, immortalized in the prompt as a permanent warning
Stopped behind a delivery truck at a red light, never moves: {"light": 3, "composition": 2, "motion": 1, "scenery": 1, "subject": 1, "peak_offset": 0.5, "clip_type": "transition", "crop_x": 50, "reason": "Stuck behind delivery truck at red light"}
-
- I liked identifying visuals: Brooklyn Bridge, State Line and 9W signs that are obvious to any cyclist
-
Solution: positive examples — show it what a 10 looks like, and what earns the score
Cruising past Brooklyn Bridge at sunset, bridge biggest at end: {"light": 9, "composition": 8, "motion": 5, "scenery": 10, "subject": 8, "peak_offset": 0.8, "clip_type": "landmark", "crop_x": 70, "reason": "Sunset Brooklyn Bridge approach, friend alongside"}
-
- Gemini often picked two clips right next to one another
- Solution: penalize nearby moments, enforce greedy selection across the ride (more below)
- Gemini picked clips where I was going 0mph
- Solution: filter clips below 5mph
Obvious in retrospect, but there’s no substitute other than looking at the clips Gemini selects, being highly opinionated about what should / should not be included, being specific enough about why, and repeating until you can’t think of anything more to improve. As Hamel writes: gathering supporting data and breaking down workflows into smaller units makes automated grading easier. You cannot teach an LLM taste if you do not have taste yourself.
Here are some outputs from an experiment I ran: I took a handful of stills from a real ride and scored them through every combination of prompts and models: from v2 to v10, the current 3.5-flash, two other Gemini models, and Opus 4.6. You can see that the prompt matters much more than more powerful foundation models.
I’ve iterated nine times on the ranking prompt so far. It is the most important file in the repo, and I treat it that way: the prompts are version controlled with a mandatory rationale explaining what failure mode the new version fixes. This prompt registry is the historical record of how an LLM’s taste for cycling visuals evolves.
This code is worth reading directly instead of describing. Check out how the prompt has grown: v2 is 20 lines and scores on two axes, v10 is 244 lines.
You are a cycling video editor reviewing GoPro handlebar-cam frames.
Your job is to flag frames worth including in a highlight reel.
VISUAL CUES for handlebar-cam footage:
Strong: motion blur on pavement, corners with lean angle, rider wheels nearby,
high-contrast light (shade/tunnels/reflections), landmarks (skylines/bridges/vistas),
sprint bounce, urban elements rushing past, weather drama, close calls, unexpected events.
Weak: steady cadence with nothing nearby, flat overcast light, stopped at lights.
Score each frame on TWO dimensions:
VISUAL (1-5): How does the frame look? Light, composition, landmarks.
ACTION (1-5): How much is happening? Effort, proximity, speed, events.
5 = lead the edit. 4 = strong candidate. 3 = filler. 2 = unremarkable. 1 = dead.
Assign ONE clip_type per frame:
"scenery" | "action" | "urban" | "climb" | "descent" | "group_riding" | "incident" | "landmark" | "transition"
Telemetry context is provided per batch — use it to disambiguate (plain frame at 350W = effort worth including).
Omit frames where BOTH visual < {min_visual} AND action < {min_action}.
Return ONLY a JSON array, no prose:
[{{"frame_index": N, "visual": 1-5, "action": 1-5, "clip_type": "...", "reason": "max 10 words"}}]
Return [] if nothing qualifies. You are a road cycling video editor curating clips for an Instagram Reel —
think Pas Normal, Pedal Mafia, GCN. The rider is a road cyclist in NYC
and Boston, riding with friends on weekends and before work. You're
looking at GoPro handlebar-cam footage from a single ride.
You will see {n_frames} evenly-spaced sample frames from one ~10-20-second
clip. Rate the clip AS A WHOLE — these are a stratified sample of what
the clip shows from start to finish. Frame 0 is the earliest, frame
{n_frames} - 1 is the latest.
═══════════════════════════════════════════════════════════════════
THE BAR
═══════════════════════════════════════════════════════════════════
This is an Instagram Reel and a calibration pass. The question for every
dimension is: *how useful is this clip to a visual edit?*
Low scores are valuable. Some clips are intentionally sampled from ordinary
coverage so the editor can see the full distribution. Do not inflate a
forgettable clip just because it was sent to you. A 1, 2, or 3 is a useful
answer when the footage is transit, blocked, visually generic, or only
interesting by telemetry.
A "good ride" generates maybe 3-8 clips that pass this bar. The other
50+ clips you'll see are filler, transit footage, or "fine but
forgettable." That is normal. Use the full 1-10 range:
10 — exceptional. Reel-stopper. The kind of moment a non-cyclist sends to friends.
7 — strong candidate. Clearly above filler. Most rides have 3-5 of these.
4 — middle. Watchable but unremarkable. Filler in a long edit.
1 — dead frame. Stopped at a light. Stuck behind a truck. Empty road.
Resist the urge to be diplomatic. Use dispersion. If 9 of these 10 clips
are 2-4s and one is an 8, that IS the right distribution — that's how
reels get made. Conversely if every clip really is a 7, give them all 7s,
but most rides do not look like that.
Telemetry can make a visually plain clip relevant, but it should mostly
raise motion or subject only when the effort is visible or narratively
clear. A hidden power spike on a visually dead street is still visually
weak; note the telemetry in reason, but keep composition/scenery/subject
honest.
═══════════════════════════════════════════════════════════════════
RUBRIC — five INDEPENDENT dimensions, each 1-10
═══════════════════════════════════════════════════════════════════
light (1-10): Light quality of the clip.
10 = golden hour with raking sidelight, wet road glare, dramatic storm
light, sun bursting through trees onto an open road
7 = clearly nice light — dappled shade, low sun warming the scene,
clean directional light with shape
4 = ordinary midday or overcast — readable but flat, no contrast play
1 = blown-out white sky with no shape, harsh shadow with no detail,
muddy underexposed mess
composition (1-10): How the frame is built — framing, leading lines, depth.
10 = strong leading lines pulling the eye, clear foreground/mid/back
layers, road curving away with intent, frame feels designed
7 = solid framing, depth visible, road or rider well-placed
4 = pointed-and-shoot, decent but no graphic interest
1 = lens flare blowing out the frame, tilted, dead foreground,
subject in a corner with no reason
motion (1-10): How much motion the clip shows — speed, lean, body english.
10 = visible sprint with bike rocking side-to-side, hard cornering with
road sweeping, descent with trees blurring past, full-send energy
7 = clear pace, things moving past the lens, mild lean or surge
4 = cruising — moving but not fast, no body english
1 = stopped, foot down, parked, crawling in traffic
scenery (1-10): Backdrop quality — what's actually in view.
10 = unmistakable iconic landmark (Brooklyn Bridge, Central Park
skyline reveal, Esplanade with Boston skyline, Zakim, Prudential)
7 = pleasant park or urban scene with character — recognizable as
a specific place, not generic
4 = ordinary streetscape or park path, unremarkable
1 = parking lot, generic suburban road, view blocked by vehicle,
nothing to see
OPENNESS GATE: scenery requires visible sky, horizon, or an open
landscape. An enclosed space — tunnel, underpass, dense unbroken
canopy with no light gaps, parking-garage interior — caps scenery
at 3 REGARDLESS of how symmetric, geometric, or "dramatic" it looks.
Geometric symmetry under elevated tracks is not scenery. A vault of
concrete is not scenery. Light playing on a tunnel wall is not
scenery. Only the moment the rider EXITS into open view counts.
subject (1-10): Is there a clear focal subject or narrative moment?
10 = unambiguous "wow" — paceline of friends close enough to touch,
bus pass at speed, dramatic close call, peak-effort summit moment
7 = clear identifiable beat — passing a rider, attacking a climb,
landmark reveal
4 = something is mildly happening, but no real story
1 = no story, no subject, just transit footage
═══════════════════════════════════════════════════════════════════
PEAK MOMENT — pick the best 3-second cut
═══════════════════════════════════════════════════════════════════
The clip you're rating is 10-20 seconds long, but the final reel cuts
each clip down to just 3 seconds. You also need to tell us WHERE in
this clip those 3 seconds should land.
Return peak_offset as a float in [0.0, 1.0] indicating where in the
clip the best 3-second window is centered:
0.0 = the first 3 seconds (start of the clip)
0.5 = the middle 3 seconds
1.0 = the last 3 seconds (end of the clip)
Think about this carefully. Examples:
- Skyline reveal as the rider emerges from a tree canopy: the reveal
lands late. peak_offset ≈ 0.7-0.9 (cut the 3s around the reveal).
- Sprint past a bus: the bus pass itself is the moment. If the bus
fills frame mid-clip, peak_offset ≈ 0.5; if late, 0.7.
- Landmark cruise where the landmark is biggest at the end:
peak_offset ≈ 0.8.
- Peak-effort climb where the rider is at full power throughout:
peak_offset ≈ 0.5 (the middle reads as the peak).
- Descent with a corner that arrives early: peak_offset ≈ 0.2-0.3.
Pick the 3-second window that, on its own, would be the most
scroll-stopping. The frames around peak_offset will be what people see.
═══════════════════════════════════════════════════════════════════
NEGATIVE PATTERNS — these should score low across multiple dims
═══════════════════════════════════════════════════════════════════
STUCK BEHIND A VEHICLE: car/truck/bus filling 20%+ of the frame while
rider sits behind it at a red light or crawls in traffic. Score motion
1-2, subject 1-2, scenery 1-3 (the vehicle is blocking the scene).
Different from "close call" (car passing fast and laterally) — that's
exciting and motion can be 7+.
DON'T INVENT LANDMARKS. A distant building, a bowling alley sign, a
parking garage, a generic brick facade is NOT an iconic landmark.
Score scenery only as high as a non-local would actually recognize the
place. If you're reaching to justify it, scenery is 4 or below.
EMPTY FILLER: bare trees, empty road, distant cyclist 30m ahead, flat
park path with nothing in the foreground = filler regardless of where
in NYC or Boston it was shot. Subject 1-2.
TUNNELS AND UNDERPASSES: any clip where the rider is INSIDE an enclosed
space — rail-trail tunnel, road tunnel, NYC elevated-track underpass,
covered bridge interior, parking-garage ramp — is a hard cap. Score
scenery 1-3, subject 1-3, regardless of how dramatic the dark/light
contrast or geometric symmetry looks. The viewer sees "stuck inside a
concrete tube," not "scenic." Two adjacent windows of the same
tunnel/underpass are also a tell — same darkness, same geometry, no
new information; the second one is filler. The reveal AT the exit
(rider emerging into open daylight/landscape) is the moment worth
scoring high — not the time spent inside.
═══════════════════════════════════════════════════════════════════
TELEMETRY CONTEXT
═══════════════════════════════════════════════════════════════════
Telemetry for the clip is provided (speed, {telemetry_fields}gradient).
Use it to disambiguate motion: {telemetry_examples}
30mph on a descent with lean = motion 8-10; 30mph on flat straight = motion 5-6.
{power_zones}
═══════════════════════════════════════════════════════════════════
OTHER FIELDS
═══════════════════════════════════════════════════════════════════
clip_type — one of:
"scenery" | "action" | "urban" | "climb" | "descent" | "group_riding" | "incident" | "landmark" | "transition"
crop_x (0-100) — where to center a 9:16 portrait crop on the 16:9 frame.
ONLY shift away from 50 when OTHER ROAD CYCLISTS are visible. Pedestrians,
runners, cars, buildings — ignore them, they crop fine in center.
- Cyclists being passed on the right → crop_x 65-75
- Friends riding alongside on the left → crop_x 25-35
- No other cyclists visible → crop_x 50
reason — one sentence describing the clip's strongest selling point
(or weakest if all dims are low). Max 12 words.
═══════════════════════════════════════════════════════════════════
OUTPUT
═══════════════════════════════════════════════════════════════════
Return ONLY a single JSON object, no prose:
{{"light": 1-10, "composition": 1-10, "motion": 1-10, "scenery": 1-10, "subject": 1-10,
"peak_offset": 0.0-1.0, "clip_type": "...", "crop_x": 0-100, "reason": "..."}}
Examples:
Sprint past a city bus, bus pass mid-clip, golden hour:
{{"light": 8, "composition": 7, "motion": 9, "scenery": 6, "subject": 9,
"peak_offset": 0.5, "clip_type": "action", "crop_x": 50,
"reason": "Golden-hour sprint past city bus, motion blur popping"}}
Cruising past Brooklyn Bridge at sunset, bridge biggest at end:
{{"light": 9, "composition": 8, "motion": 5, "scenery": 10, "subject": 8,
"peak_offset": 0.8, "clip_type": "landmark", "crop_x": 70,
"reason": "Sunset Brooklyn Bridge approach, friend alongside"}}
Stopped behind a delivery truck at a red light, never moves:
{{"light": 3, "composition": 2, "motion": 1, "scenery": 1, "subject": 1,
"peak_offset": 0.5, "clip_type": "transition", "crop_x": 50,
"reason": "Stuck behind delivery truck at red light"}}
Hard climb past the San Remo with peak power mid-clip:
{{"light": 5, "composition": 7, "motion": 7, "scenery": 8, "subject": 8,
"peak_offset": 0.55, "clip_type": "climb", "crop_x": 50,
"reason": "Anaerobic climb framing San Remo towers"}}
Skyline reveal emerging from tree canopy late in clip:
{{"light": 4, "composition": 8, "motion": 5, "scenery": 9, "subject": 8,
"peak_offset": 0.75, "clip_type": "scenery", "crop_x": 50,
"reason": "Canopy emerges revealing Manhattan skyline"}}
Rail-trail tunnel, symmetric dark interior with bright exit far away:
{{"light": 6, "composition": 7, "motion": 5, "scenery": 2, "subject": 2,
"peak_offset": 0.9, "clip_type": "transition", "crop_x": 50,
"reason": "Inside a dark tunnel — enclosed space, no sky"}}
Under NYC elevated tracks, symmetric girder geometry, no visible sky:
{{"light": 5, "composition": 7, "motion": 4, "scenery": 2, "subject": 3,
"peak_offset": 0.5, "clip_type": "urban", "crop_x": 50,
"reason": "Underpass geometry only — not scenery"}} +++
name = "gemini_scan"
version = "v10"
model = "gemini-3.5-flash"
date = "2026-06-12"
commit = "uncommitted"
supersedes = "v9"
placeholders = ["n_frames", "power_zones", "telemetry_fields", "telemetry_examples"]
rationale = """
v10 changes the prompt contract from "find clips above a bar" to
"calibrate a distribution." The pipeline now sends periodic coverage
samples and keeps low-scored rubric results, so Gemini should score
ordinary/transit clips honestly instead of treating low scores as
failed outputs. Carries forward v9's openness gate, peak_offset,
rubric, and output schema.
"""
+++ From Clips to a Reel
The candidate generation process ranks every single potential three second clip (around ~1000 per hour). The simplest approach to fill a 60 second reel is to select the top 20 ranked candidates. That results in an extremely repetitive edit, because the top ranked candidates are often right next to one another. A Bear Mountain descent is ~7 minutes, which results in ~100 different clips of a fast descent with beautiful views of the Hudson, all of which score highly on a per-clip basis, but are not independently interesting.
Teaching a LLM to accurately score one clip is a challenge, but at least each clip can be evaluated on its own. Candidate selection is even harder: How DOES a great editor select the footage to tell a great story? From my perspective, here are traits of a great NYC cycling edit:
- Set the tone with the start and finish: for me, usually slow pedaling up West Side Highway, and back down through the skyscrapers of Hudson Yards
- Order the clips chronologically, sampling uniformly across the ride (within reason)
- Include landmarks, especially landmarks beloved by cyclists
- Clips within a minute of one another are probably repetitive
- Clips going <5mph are probably boring
- Clips with friends in them are more interesting
- Shoot for diversity: include the steepest climb, the fastest descent, the most powerful sprint, the sun shining through the forest in the Palisades, the NYC concrete jungle, The State Line sign, 9W Market
Similar to scoring, I’ve rebuilt the candidate fusion step six times, with each version fixing something from a prior iteration. The current selection process works like this:
- “Must include” manual labels are picked first
- A Gemini LLM narrative pass picks 20 clips to best tell the story of the ride, boosting “cross-source agreements” (if a human label + telemetry + Strava + Gemini all agree that a clip is interesting)
- Greedy re-ranking with a crowding penalty to avoid clips too close to something I’ve already picked
- First, a “quality” phase that picks best-first until the best remaining candidate is net-negative with crowding penalty
- Second, a “coverage” fill that guarantees the exact clip count by placing the liveliest available clips into the biggest timeline holes.
Here is the LLM narrative pass prompt and crowding penalty:
You are editing a cycling highlight video. Select exactly {n_clips} clips
from the candidates below for a {budget_secs:.0f}-second highlight reel.
COUNT — READ THIS FIRST:
- Your answer MUST contain EXACTLY {n_clips} indices. Not fewer.
- The reel is {budget_secs:.0f} seconds at ~3 seconds per clip, so it needs
all {n_clips} clips to fill the runtime. A short list leaves dead air that
gets padded with worse clips — so returning fewer than {n_clips} makes the
video WORSE, not tighter.
- If you think fewer clips tell a better story, you are wrong for THIS format:
pick the {n_clips} best and order them well.
SELECTION RULES:
- Clips play in chronological ride order — pick clips that tell the ride's story as it unfolded.
- Ensure VARIETY in clip_type — no more than 3 of the same type.
- Spread selections across the ride timeline — don't cluster in one section.
- USER LABELS ARE EDITORIAL GROUND TRUTH. Any candidate whose sources include "label"
with visual>=4 or action>=4 represents a moment the rider personally flagged as
important. Strongly prefer these — include them unless doing so would badly break
the variety/spread rules above. If you must drop one, prefer dropping gemini-only
candidates instead.
- Prefer clips confirmed by multiple sources (telemetry + gemini + label).
- {layout_guidance}
CANDIDATES (sorted by ride time):
{candidate_text}
Return a JSON array of EXACTLY {n_clips} candidate indices, in the order they
should appear. Example shape (yours must have {n_clips} entries):
[2, 5, 9, 14, 17, 21, 26, 30, 33, 38, 41, 47, 52, 55, 58]
Return ONLY the JSON array, nothing else. _PROXIMITY_LAMBDA = 6.0 # weight on the crowding penalty
_PROXIMITY_CROWDING_CAP = 2.0 # cap the sum so one big cluster can't lock out
# the whole neighborhood. Past two near-by picks
# the penalty stops growing.
def _proximity_crowding(t: float, picked_ts: list[float], tau: float) -> float:
"""Sum of exponential proximity weights against already-picked clips.
crowding = min(Σ exp(-|t - t_i| / tau), _PROXIMITY_CROWDING_CAP).
Adjacent picks contribute ~1.0, far-apart picks contribute ~0.
Capped so a dense cluster of must_includes can't lock the greedy
out of an entire region of the ride.
"""
if not picked_ts:
return 0.0
raw = sum(math.exp(-abs(t - tp) / tau) for tp in picked_ts)
return min(raw, _PROXIMITY_CROWDING_CAP)
def _diversity_tau(ride_times: list[float], n_clips: int) -> float:
"""Characteristic decay distance for the proximity penalty.
Roughly the average gap between picks at full budget. A pick exactly
at this distance contributes 1/e ≈ 0.37 of the crowding it would at
zero distance.
"""
if not ride_times or n_clips <= 0:
return 60.0
span = max(ride_times) - min(ride_times)
return max(60.0, span / max(n_clips, 1))
# Applied when ranking each remaining candidate:
eff = seg.score + _label_boost(seg) - _PROXIMITY_LAMBDA * crowding The 60 second July 12th highlight video at the top of this post is the output. Here’s a visual of the input: every candidate clip from the July 12th ride to 9W Market, plotted by time and score. The sprint you see as the closing shot is the orange dot on the right; the black SUV is the one at the bottom, ranked 70th of 70. Note that we didn’t pick the top 20 clips from the highest scoring section, but instead we selected 20 highly ranked clips spread somewhat evenly across the ride.
What’s Next?
I’m now three months of Claude Max, ~1000 miles, and $60 in Gemini API into this project. I have a YouTube channel with minimal traffic and a few pity subscribers, mostly related to me or in my WattsApp groups. I’ve gotten a few “wow, cool!” on my personal Instagram but again… mostly pity. I’m starting to feel done with this project, which scares me a bit! When I watch my videos, increasingly, they feel “right” to me, which might also mean my taste has plateaued and I’m complacent. I haven’t been iterating as quickly, and iteration is the whole key.
Obviously, I have registered a domain and have a harebrained business plan in my head. Hypothetically I could charge cyclists per ride to upload video footage + a .fit file and get back a finished edit.
Unfortunately, I think the TAM for customers who:
- Road bike frequently
- Are willing to buy a GoPro, Garmin, mount both to their bike, charge and record everything
- Really want 60 second edits of their footage for Strava + Instagram
- Do NOT want to make those edits themselves
Is one person, and it’s me.
But guess what! I love cycling, and I love building for me! Through a decade of managing data scientists, I have learned people are 10x more willing to go deep on something they are passionate about. This project scratched my itch. I’ve learned a ton about building LLM pipelines and coding agents. This post hasn’t even scratched the surface on what I’ve learned about:
- Working with video files:
.LRVproxy files, 5.3k HEVC, inter-frame compression, ffmpeg, and parallel processing to downscale and decode frames, VideoToolbox, RBGA overlays with Pillow. - Gemini billing, API failures, caching prompt results, identifying the cheapest model relative to a performance bar, evaluating foundation models within pipelines and as their own discrete steps, general token cost management.
- Clock drift across GoPro + Garmin: the most brutally frustrating part of this project was when my offset math and the ride overlay felt off by a few seconds. I turned off wifi + bluetooth on the GoPro to save battery, and the clock drifts a few seconds per week, and occasionally within a ride.
- Cropping a 16:9 frame (landscape) for a 9:16 (portrait) reel, and identifying the best place to crop the portrait (I only crop away from the middle to highlight other road cyclists in the frame).
- Adding a blur-to-title opener + outro recap card.
- “Out of distribution” clips: a deer, a dog doing Central Park laps, anything interesting for reasons that have nothing to do with cycling.
- Calibrating LLM outputs (I used to have a
lightdimension that returned a 4 no matter what).
Some of my to-do list involves better evals for the learned ranker, testing non-Gemini models, learning more about video editing (color grading? motion blur? audio edits?). I’ve open-sourced ride-recap and will share it in a few communities, to see if anyone jumps on it or can push me in a new direction. New frontier models will come out, I’ll see what I can improve. There’s always more to do.
I now have personal software that I use a few times a week that brings me joy. The quality came through the iterations. This wouldn’t have been possible without coding agents, but it also wouldn’t have been possible without the version of “taste” I believe in now. Taste is not what you start with, it’s what you develop by watching thousands of three second clips, iterating, reviewing v25, being unsatisfied and shipping v26.

