Race Day Copilot

Verified pacing, not vibes

Race Day Copilot

Photograph a race course map — get a km-by-km pacing plan checked against five deterministic rules before it's ever shown to you, plus a locally fine-tuned model that produces the same plans for free.

By Jeremy Lee

Why

Ask a chatbot for a marathon pacing plan and you're trusting it to sum 42 split times correctly, reason about hills it can't see numerically, and not quietly hand you a negative-split strategy that almost nobody actually achieves. Race Day Copilot never trusts a plan on vibes: every plan is checked in plain code against five rules before you see it — coverage, arithmetic sum, per-km plausibility against the real course profile, fade direction, and a heat-adjustment formula derived from a real Boston Marathon finding.

📷

No vision API key

Local OCR + Claude reads the course-map photo into a structured profile — refuses non-course photos.

Five deterministic checks

Coverage, sum, plausibility, fade, and heat — every violation a plain-English sentence, checked before you ever see the plan.

🌡️

Real heat-tax formula

Heat adjustment derived from this portfolio's own Boston Marathon finding: ~1 min slower per °F on the field median.

🧠

Locally fine-tuned

A LoRA-tuned Qwen2.5-3B distills the same plans — runs on a laptop, $0 marginal cost.

The fine-tune, honestly benchmarked

Held out by scenario id (70 scenarios), zero-shot/no-retry, identical prompt for all three systems:

SystemParse successVerifier pass rateLatency
Base Qwen2.5-3B (no fine-tune)55.7%0.0%17.5s
+ LoRA (this project)95.7%0.0%*53.7s
Claude (teacher, zero-shot)90.0%75.7%157.7s
📊 *Not a failure story — read the per-check table. LoRA reaches near-parity with the teacher on coverage, hill plausibility, and the heat formula (all <5% failure). The real gap is exact split-sum arithmetic (82.9% miss the 90s tolerance) and a self-consistency mismatch in the fade field — a genuine LLM arithmetic limitation, not a bug. Full breakdown in the writeup.
⚠️ This was the hardest fine-tune of five. Getting here took four training attempts across two days: a sequence-length miscalculation, a memory bug that faked being alive, two loss-divergence incidents, and a custom gradient-clipping training script built from scratch because mlx_lm.lora's CLI has none — which still didn't fully eliminate the instability. The published model is the best checkpoint from ~400 iterations of confirmed-clean training, published deliberately rather than chasing a fifth attempt. The full story is in the writeup — it's the most interesting part of this whole project.

Training loss curve

How it works

  1. Photograph a course map — local OCR + Claude extracts distance, hill segments, and a confidence score.
  2. State a goal time, temperature, and experience level — retrieval pulls the 14 most relevant pacing-research knowledge cards.
  3. Claude (or the local LoRA model) generates a km-by-km plan from the identical assembled prompt.
  4. The plan is checked against five deterministic rules before you ever see it — a failure regenerates with the violation fed back as plain English.
🔒 Everything runs locally except the claude -p calls for course extraction and generation — no metered API, no vision API key. See the full writeup for the complete training saga, including four real incidents and the two bugs found building the benchmark itself.
View on GitHub Read the full writeup