-
-
Ten Team USA sport families ranked by Olympic vs Paralympic coverage gap, with medal totals on every row.
-
The Visible Parity Auditor in action: a safe draft is approved; a draft with a specific score and prediction is blocked.
-
A complete bridge story: Olympic summary on the left, Gemini-written Paralympic companion on the right, ten safety checks visible.
-
Landing page — the manifesto plus five "if you follow X, explore Y" prompts that open a Paralympic story fans almost missed.
-
A second bridge story for Alpine skiing — same structure, different sport, real data on both sides.
-
Four-step pipeline: corpus ingest → Gemini classification → parity gap computation → bridge card generation with visible safety audit.
-
System architecture: Next.js dashboard, FastAPI routes, three Gemini-backed agents, deterministic safety services, SQLite data layer.
-
Google Cloud runtime: Cloud Run hosts both services, Cloud Build ships from source, Secret Manager holds the Gemini key.
-
Bridge generation sequence: user click → snippet gather → Gemini left + right cards → hard + soft auditor checks → approved card.
Inspiration
Cheering for Team USA shouldn't end at the Olympic side. Every two years the Olympic stories saturate every fan feed — but the Paralympic stories sitting right beside them, with the same Team USA pride and often the same family of sports, mostly stay out of view. We wanted a fan-first discovery surface that fixes the imbalance: not by predicting medals or ranking athletes, but by quietly pointing fans toward the second story they almost missed.
What it does
StoryBridge has three surfaces:
- Story Gap Dashboard — ranks Team USA Winter sport families by how unevenly the Olympic and Paralympic sides are covered, so the gap is visible, not implied.
- Bridge Card Generator — for any sport family, generates a sport-level Paralympic companion card beside the Olympic story fans already know. Conditional language only ("may", "could"), zero athlete names.
- Visible Parity Auditor — every generated card shows ten safety checks beside it (five deterministic, five from Gemini). Approval is a public moment, not a hidden gate.
The product never names individual athletes, never quotes a finish time, never predicts a medal — by design and by audit.
How we built it
- Gemini 2.5 Flash Lite powers three jobs: a corpus classifier (sport family, movement type, story type), a bridge-card generator, and a soft auditor that complements the deterministic safety pass.
- FastAPI (Python 3.12, async) for the backend; Next.js 16 + Tailwind 4 + TypeScript for the frontend.
- Google Cloud Run hosts both services, scaled to zero between requests. Cloud Build ships them straight from source. Secret Manager holds the Gemini API key — never baked into an image.
- Defense-in-depth safety: a regex + spaCy NER sanitizer at ingest, a Gemini-extracted athlete-name blocklist that re-scrubs raw documents, and the visible auditor at output. All three layers must pass before a card displays.
- Permitted-data discipline: only
teamusa.com/teamusa.organd US-scoped Wikipedia pages ("United States at the X Winter Games"). Only finish placements (1st/2nd/3rd) and medal counts surface as aggregate context. - Honest provenance: every API response labels itself
gemini,stub, orcached. Source receipts include excerpts and URLs (athlete-profile URLs are redacted before render so the slug never leaks).
Challenges we ran into
- Data Strategy compliance under real pressure. The hackathon's NIL rule is strict — zero athlete names anywhere visible. We caught one leak the easy way (athlete profile URLs encoded names in the slug, e.g.
/athletes/<name>); the auditor was passing the body text but the URL was rendered as a clickable href. We added a redactor and a regression test so it can't come back. - Silent redirects polluting the corpus. Several
teamusa.orgarticle URLs returned the generic news index instead of the actual article. The classifier saw 15 identical "Stay informed with the latest news…" docs and bucketed them all under one sport, faking a 0-vs-15 parity. We detect that signature now and skip withskipped_redirect. - Sample-size honesty. Aggregating by
(sport_family, story_type)produced 0-or-1-doc buckets that forced parity scores to 0.00 or 1.00. We aggregated up tosport_familyonly, kept the dominant story-type as a hint, and the rows became interpretable. - Visible-but-not-distracting auditor. A safety surface is a feature only if a fan can read it. Five hard checks plus five soft checks, each with one-line evidence — readable in five seconds, not a page of JSON.
Accomplishments that we're proud of
- The Visible Parity Auditor stress-tests cleanly: feed it deliberately toxic input ("athlete name + finish time + Olympic rings + 'will win'") and it returns
blockedwith each violation cited as evidence. - Live on Cloud Run with both services scaled to zero — empty-traffic cost is effectively $0/month.
- 108 automated tests including regression coverage for the NIL slug fix and the auditor's blocking path.
- Apache 2.0-licensed public repo with a published parity formula, a 46-source corpus inventory, and a methodology doc.
What we learned
- Safety is a product feature, not a filter. Showing the auditor's checks beside every card is what makes the safety story believable. Hiding them would have made the same backend feel like a black box.
- Gemini structured outputs are reliable when the schema is tight, brittle when it isn't. Constraining
sport_familyto a fixed list and validating with Pydantic v2 changed the failure rate from "occasional" to "cached fallback for the rare hiccup." - Defense-in-depth pays off when one layer fails. Our sanitizer didn't know about Wikipedia roster pages; spaCy NER caught most names; Gemini extraction caught the rest. No single layer was enough on its own.
- Honest 1.00 parity tells a story. Sports without a Paralympic counterpart (luge, bobsled, skeleton) honestly read 1.00 — and labeling the bridge as
(proposed)makes that structural gap a feature, not a bug.
What's next for StoryBridge
- BigQuery + Cloud Storage as the production warehouse — the SQL store is already behind a Protocol, so the swap is dependency-injection, not a rewrite.
- LA28 Summer corpus. We pivoted Winter for Milano Cortina 2026 fit; Summer-side URLs come online closer to the 2028 cycle.
- Multi-cycle parity scores with decay weighting across 2018 / 2022 / 2026, so the dashboard reflects momentum, not a single-cycle snapshot.
- Newsroom mode: rate-limited public read API so editors can pull a daily "what's the parity gap right now?" feed straight into a CMS.
Built With
- artifact-registry
- cloud-build
- cloud-run
- fastapi
- gemini
- google-cloud
- httpx
- mypy
- next.js
- pydantic
- pytest
- python
- ruff
- secret-manager
- spacy
- sqlite
- tailwindcss
- typescript
- uv
- vertex-ai


Log in or sign up for Devpost to join the conversation.