Citius Chrono

Results for 437 finishers rebuilt from finish-line video in a day, then published by the head coach.

What was the problem?

My running club’s head coach, Dany, organized a college cross-country race, and I was in charge of publishing the results. The finish was timed by hand: one volunteer saved a time each time a runner crossed the line, and two of us wrote down bib numbers further down the chute. When runners arrived in groups and swapped places, their times swapped too, and nobody could tell which results were wrong.

What was the idea?

I knew two iPads had filmed the finish, so I asked for the videos. I figured I could set up a system that analyzes them: find the exact moment each runner crosses the line, and read each runner’s bib number from the video.

What did I build?

I designed a one-off Python pipeline for this one race, from the two videos and the registration lists, and used Claude Code to write it.

Timing. I lined up the two recordings by their sound and read the race clock filmed in the video, so every frame gives a race time.

Tracking. A first try with Apple’s Vision framework kept losing runners between frames, so I switched to tools made for video: RF-DETR finds each runner, and ByteTrack follows them across the line.

Bibs. Gemini reads a crop of each bib. A bib counted as confirmed only if it read clearly, stayed the same across frames, was on the registration list and appeared once in the race. The rest, such as a bib hidden by an arm or a runner hidden in a pack, went on a review list, and I settled the last 20 or so myself from the frames.

Then I shipped a small web app to replay the finish, with both videos in sync and the ranking filling in as runners cross, so friends could watch it from a link.

Rebuilding the results, in one day: on my two Macs, the two iPad videos were lined up by sound, RF-DETR and ByteTrack found the frame where each runner crossed the line, and Gemini read small crops of each bib. A bib counted as confirmed only if it read clearly, stayed the same across frames, was on the registration list and appeared once in the race. The rest went on a review list, and I settled the last 20 or so myself. The results and the videos went to Cloudflare. When someone watches the replay: the browser, behind a private access code, asks a Worker for the pages, and the Worker streams the videos from R2.

USER’S DEVICECLOUDFLAREMY MACSOUTSIDE SERVICESABbib readsCconfirmedDthe restEsettledFTwo iPad videosLined up by soundRunner trackingRF-DETR, ByteTrackGeminiReads bib cropsBib checksFour checks per bibMy reviewThe last 20 or soResultsPlaces and timesThe replayResults, videos in R21streams2BrowserSynced replayWorkerServes the pages

When someone watches the replay

  1. 1The browser, behind a private access code, asks a Worker for the pages.
  2. 2The Worker streams the videos from R2, so you can scrub.

What happened?

Both results lists ended up with the same 437 finishers: the one we wrote by hand during the race, and the one I rebuilt from the video. 430 of the 437 times agreed within half a second. Most of the seven that didn’t were runners in packs, where the hand-written order had swapped runners and so swapped their times. In four places the order was different. I watched those four finishes in the replay, and the video order was the right one.

I sent the corrected list to Dany, the head coach, who published it.

Say hello

Questions about this project, or hiring? Send me a note.

hello@victorc.dev