Project Info
This project did not submit a demo video on Devpost.
Inspiration
Justin’s father is low vision, and watching sports with him made one problem impossible to ignore: broadcasts are still designed for people who can clearly see the screen. Soccer commentary captures the emotion of a match, but it often skips the visual context that sighted fans take for granted. A commentator might yell “what a chance,” but a low-vision fan may still not know where the ball was, who was open, which direction the attack was moving, or why the stadium reacted. Existing accessibility solutions for sports are still limited. Most tools focus on subtitles, screen readers, or generic text-to-speech, but those do not solve the core problem of live sports: the most important information is spatial and visual. Fans need to understand the field, the movement, the pressure, and the moment, not just hear a transcript. This is not just one family’s problem. Vision impairment affects approximately 2.2 billion people worldwide, and sports remain one of the most shared cultural experiences in the world. MatchVision was built to make that experience more accessible, independent, and emotionally complete.
What it does
MatchVision is a voice-first accessibility companion for blind and low-vision soccer fans. It acts as a missing visual layer for the match, turning soccer video into concise audio-descriptive commentary and natural voice Q&A. Instead of giving generic commentary, MatchVision explains the visual details that matter: where the ball is who has possession which direction the attack is moving where pressure is coming from who has space what key event just happened why the crowd reacted Users can ask questions like: “What just happened?” “Where is the ball?” “Who has space?” “Give me tactical detail.” MatchVision responds with short, spoken answers designed for live play. The key idea behind MatchVision is that not every part of a frame matters equally. We used human gaze heatmaps to identify the regions people naturally focus on during soccer clips, then used those heatmaps to guide the vision-language model toward the most important parts of the play. We also used human annotations from Terac to improve the captions themselves, teaching the model to produce descriptions that are useful for accessibility: spatially clear, concise, grounded, and focused on the moment. Conceptually, each visual region is weighted by human attention: $$ \text{Attention Weight} \propto \text{Heatmap Intensity} $$ Then the captioning objective is guided by both visual attention and human caption quality: $$ \mathcal{L}{\text{total}} = \mathcal{L}{\text{caption}} + \lambda \mathcal{L}{\text{heatmap}} + \beta \mathcal{L}{\text{human}} $$ This lets MatchVision learn not only what is in the frame, but what parts of the frame matter most to explain.
How we built it
We built MatchVision as a full human-in-the-loop accessibility pipeline. First, we collected gaze data from users watching soccer clips. Using webcam-based eye tracking with WebGazer.js and MediaPipe FaceMesh, we took advantage of the human annotation marketplace Terac to capture where people looked during each play. We aggregated those gaze points into heatmaps, which showed the visually important regions of the field over time. These heatmaps became an attention signal for the model, helping it focus more on the hottest regions of the frame instead of treating the entire video equally. Using Terac, we also collected human annotations for audio-descriptive commentary. Annotators reviewed soccer moments and ranked or improved captions based on what would be most useful to a blind or low-vision fan. We focused on features like ball location, direction of attack, key-event coverage, concision, and hallucination avoidance. We then used these two human signals together to fine-tune a vision-language model. The heatmaps guided the visual side of the model by emphasizing the parts of the frame humans cared about most, while the Terac captions guided the language side by teaching the model what high-quality accessibility commentary sounds like. Our pipeline looked like this: Collect soccer clips. Record human gaze while users watch each clip. Convert gaze points into frame-level heatmaps. Use Terac annotators to create and rank accessibility captions. Fine-tune a vision-language model with heatmap-weighted frames and human-improved captions. Generate audio-descriptive commentary. Let users ask voice questions about the match. On the product side, we built the app with JavaScript, HTML, CSS, and Node.js. We created a Chrome Extension using Manifest V3 for gaze-controlled video interaction, a web app for audio-descriptive commentary and Q&A, and API routes for storing labels, sessions, and context. Technologies we used include: JavaScript / HTML / CSS Node.js WebGazer.js MediaPipe FaceMesh Terac MCP Redis / Upstash Deepgram LLM/VLM APIs Python analytics scripts JSON match timelines and event logs
Challenges we ran into
One of the biggest challenges was defining what “good” actually means. A caption can be grammatically correct and still be bad for accessibility. If it says “a dangerous chance develops” but does not say where the ball is, who is attacking, or why the chance is dangerous, it fails the user. We had to design our annotation rubric around accessibility quality instead of generic caption quality. Another challenge was aligning gaze heatmaps with model attention. Human gaze data is noisy, especially when collected through webcams in different lighting conditions and screen setups. We had to smooth the signal, calibrate users, aggregate data across sessions, and think carefully about how to turn heatmap intensity into a useful training signal. Sports video itself was also difficult. Soccer is fast, crowded, and often ambiguous. The ball is small, important actions happen off-ball, and camera angles change constantly. We had to make the model concise while still grounded, and we had to avoid hallucinations because incorrect descriptions can be worse than no description at all. We also had to combine a lot of moving parts into one coherent system: gaze tracking, heatmaps, Terac annotation, caption fine-tuning, voice Q&A, Redis-backed storage, and browser-based playback. Making all of those pieces feel like one product instead of separate demos was one of the hardest parts of the project.
Accomplishments we're proud of
We are proud that MatchVision is grounded in a real accessibility need. The project began with Justin’s father, but the problem extends to millions of blind and low-vision fans who are excluded from the full visual experience of live sports. We are especially proud of building a human-in-the-loop model improvement pipeline. Instead of just prompting a model to “describe this soccer clip,” we used two forms of human feedback: gaze heatmaps to show where people look, and Terac annotations to show what people find useful. That made the system more intentional and more accessibility-focused. We are also proud of making the product voice-first. A low-vision user should not need to navigate a complicated visual interface to understand a visual moment. They should be able to ask a question naturally and hear a useful answer immediately. Finally, we are proud that MatchVision reframes sports accessibility. We are not trying to replace commentators. We are building the missing visual layer that helps fans understand the parts of the match that normal commentary assumes they can already see.
What we learned
We learned that accessibility is not just a feature. It changes the entire objective of the system. A normal video captioning model optimizes for fluent descriptions. MatchVision has to optimize for trust, timing, spatial clarity, and usefulness. We also learned how powerful human data can be. Heatmaps gave us a way to teach the model what parts of the frame mattered visually, while Terac annotations taught it what kinds of descriptions mattered linguistically. Combining those two signals helped us think about model fine-tuning in a more human-centered way. We learned that live sports are uniquely hard because the important context is constantly changing. It is not enough to identify objects. The system has to understand movement, direction, pressure, spacing, and why a moment matters in the match. Most importantly, we learned that personal motivation makes technical decisions sharper. Thinking about whether this would actually help Justin’s father made us focus less on flashy AI output and more on clarity, reliability, and dignity.
What's next
Next, we want to move MatchVision closer to real-time live match support. Our goal is for blind and low-vision fans to use it during a live broadcast, not only on preprocessed clips. We also want to collect more gaze and caption data from blind and low-vision soccer fans directly. Their feedback should define what the model optimizes for. We plan to expand the Terac annotation pipeline, improve the heatmap-guided fine-tuning process, and evaluate the system on more matches, camera angles, and levels of play. On the product side, we want to improve personalization. Some users may want one-sentence updates, while others may want tactical detail or beginner-friendly explanations. MatchVision should adapt to each fan’s preferences. Long term, we want to expand beyond soccer. The same approach could support basketball, tennis, racing, concerts, theater, and any live visual experience where important context is trapped on screen. Our vision is simple: when the crowd erupts, everyone should know what happened, where it happened, and why it mattered. MatchVision is our step toward making live sports accessible to everyone.
MatchVision
Commentary tells you the game. MatchVision lets you see it.
MatchVision is a voice-first accessibility companion that gives blind and low-vision soccer fans the missing visual layer of a soccer match: ball location, player positioning, direction of attack, and why key moments matter.
What's in this repo
Two parts that share one accessibility mission:
- Chrome extension (
extension/) — a gaze-controlled zoom/pan tracker that works on any web video, plus an always-on Claude voice agent. Say "Match Vision, what just happened?" and Claude answers out loud via Deepgram TTS (browser TTS as fallback), and can drive the tracker itself (zoom in, reset, follow the ball) via tool calls. - Web app (
src/,local-server.mjs) — the original ADC (audio description) demo: structured match-moment timelines, a voice/text Q&A flow, a Terac annotation lab for collecting human accessibility labels, and an eval dashboard showing measured improvement from those labels.
Run the demo
Web app
npm run dev
Open:
- Main demo: http://localhost:5173
- Annotation Lab: http://localhost:5173/annotate.html
- Eval Dashboard: http://localhost:5173/eval.html
No install or API keys required for the static MVP — it falls back to a deterministic local description and browser speech APIs. Add GEMINI_API_KEY/ANTHROPIC_API_KEY (see below) for real model-generated descriptions.
Chrome extension
cp extension/secrets.example.js extension/secrets.jsand fill inMV_ANTHROPIC_KEY/MV_DEEPGRAM_KEY.extension/secrets.jsis gitignored — it's never committed, so real keys never hit GitHub.- Open
chrome://extensions, enable Developer Mode, "Load unpacked", selectextension/. - Open any page with video (YouTube, a broadcast stream, etc.) and click the MatchVision icon.
- Click the mic button and talk — Claude answers (spoken via Deepgram, falling back to browser TTS), and can zoom/pan/reset the tracker for you.
Current status
- Gaze-controlled video zoom/pan extension (WebGazer-based), with a Claude voice agent layered on top that can both answer questions and control the tracker.
- Structured match-moment web demo with voice/text Q&A and spoken fallback.
- Terac annotation lab: drag-rank 5 commentary variations per clip, collect human accessibility labels.
- Real Terac MCP integration (
scripts/terac-agent.mjs) that creates a paid labeling opportunity, polls for submissions, and approves them against a budget guardrail — not just a UI mockup. - Gemini/Claude-generated commentary candidates (
scripts/generate-candidates.mjs) feed the annotation lab instead of hand-written examples. - Prompt-optimization pipeline that learns a "champion prompt" from Terac preference labels, plus a DPO dataset exporter for fine-tuning.
- Analytics-replay data source: real StatsBomb matches converted to structured moment timelines via
kloppy+socceraction(xT + VAEP), synced to real broadcast footage — no live VLM required. npm run check/npm run metrics/npm run eval-rankerpass; see numbers via those scripts rather than stale copy here.
Validate data
npm run check
npm run metrics
Analytics-replay clips (StatsBomb ground truth, no live VLM needed)
Alongside the live-video pipeline, the demo includes a second data source: real StatsBomb open-data matches converted to a structured moment timeline via kloppy + socceraction (xT + VAEP), with no broadcast video required. See analytics/build_state_frames.py.
Currently wired into data/clips.json:
- Turkey vs Italy, Euro 2020 group stage (
turkey_vs_italy_euro2020_analytics) — ticker only, no video. - Argentina vs France, 2022 World Cup Final (
argentina_vs_france_wc2022_final_analytics) — ticker synced to FIFA's official full-match YouTube upload via avideo_offset_secondskickoff offset.
Regenerate a timeline (requires the analytics/.venv — Python 3.12, see analytics/requirements.txt):
cd analytics && source .venv/bin/activate
python fit_models.py # fits xT + VAEP once, caches to analytics/cache/
python build_state_frames.py # default: Turkey vs Italy
python build_state_frames.py --match-id 3869685 --out ../data/analytics/argentina_vs_france_wc2022_final_timeline.json
AI generation credentials
For the annotation candidate generator (scripts/generate-candidates.mjs) and /api/describe, use either Gemini or Anthropic — both support vision (frame analysis) and text generation:
# Gemini
GEMINI_API_KEY=your_google_ai_studio_key
GEMINI_MODEL=gemini-2.5-flash # optional, this is the default
# Anthropic
ANTHROPIC_API_KEY=your_anthropic_key
ANTHROPIC_MODEL=claude-haiku-4-5-20251001 # optional, this is the default
GOOGLE_API_KEY is also accepted as an alias for GEMINI_API_KEY. Set LLM_PROVIDER=gemini or LLM_PROVIDER=anthropic to force one; otherwise both local-server.mjs and generate-candidates.mjs auto-detect (Gemini first if both keys are set).
The Chrome extension's voice agent calls the Anthropic API directly from the browser using MV_ANTHROPIC_KEY from extension/secrets.js (gitignored — see extension/secrets.example.js). TTS uses MV_DEEPGRAM_KEY from the same file, falling back to browser TTS if it's unset or the call fails.
Terac fine-tune pipeline (human labels → better commentary prompt)
See docs/TERAC_FINETUNE_PLAN.md for the full design. Short version — zero labels to a champion prompt:
-
Generate real AI candidates (replaces hand-written commentary variations):
GEMINI_API_KEY=your_key node scripts/generate-candidates.mjs # or Anthropic instead: LLM_PROVIDER=anthropic ANTHROPIC_API_KEY=your_key node scripts/generate-candidates.mjs # or one clip at a time: GEMINI_API_KEY=your_key node scripts/generate-candidates.mjs --clip yt_eng_cro_12 # sanity-check without burning quota / without a key at all: node scripts/generate-candidates.mjs --dry-runWrites real Gemini or Anthropic outputs (5 prompt strategies per clip) into
data/annotation_tasks.json, tagged withgeneration_provider/generation_model. -
Collect Terac rankings — open
annotate.html, rank the 5 real candidates per clip. Locally this POSTs to/api/labelsand is stored indata/labels.local.json(gitignored) bylocal-server.mjs; hosted Terac sessions use the same shape. For a real paid Terac run, seescripts/terac-agent.mjs(npm run terac), which launches and manages the opportunity end-to-end via Terac's MCP API. -
Build the preference dataset:
node scripts/build-preference-dataset.mjs --api=http://localhost:5173 # -> data/training/preference_pairs.jsonl (+ summary.json) -
Learn the champion prompt (Phase 3A — no GPU, no fine-tune budget needed):
node scripts/optimize-prompt.mjs # -> data/prompts/champion_prompt.txt + data/prompts/champion_eval.jsonFinds the prompt strategy that wins most often, extracts the linguistic patterns of winning vs. losing commentary (ball-location rate, direction mentions, hedging, sentence length), and bakes them into an explicit system prompt. If
data/training/preference_pairs.jsonldoesn't exist yet, this exits cleanly with instructions instead of crashing.local-server.mjsautomatically loadsdata/prompts/champion_prompt.txtat startup (if present) and uses it as the system prompt for every/api/describecall, overriding the client-sent default — no code changes needed once the file exists. The response body includesusedChampionPrompt: true/falsefor debugging. -
Export a DPO fine-tune file (Option B — stronger, needs a training budget):
node scripts/export-dpo-dataset.mjs # -> data/training/dpo_dataset.jsonl, one {"prompt","chosen","rejected"} object per line node scripts/export-dpo-dataset.mjs --min-rank-gap=2 # keep only high-confidence pairsReady for the OpenAI fine-tuning API, a local
trlDPOTrainer run, or the Anthropic fine-tuning API once available.
API keys needed, by step
| Step | Key | Where |
|---|---|---|
| Generate candidates | GEMINI_API_KEY (or GOOGLE_API_KEY) or ANTHROPIC_API_KEY | .env, read by scripts/generate-candidates.mjs |
| Collect labels | none | local — local-server.mjs writes data/labels.local.json |
| Real Terac labeling run | TERAC_API_KEY | .env.local, read by scripts/terac-agent.mjs |
| Build preference dataset | none | reads from the local server's /api/labels + /api/sessions |
| Optimize prompt | none | pure text-pattern analysis over preference_pairs.jsonl |
| Live ADC/Q&A with champion prompt | one of DASHSCOPE_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY | .env, read by local-server.mjs |
| Export DPO dataset | none | pure reshape of preference_pairs.jsonl |
Every step degrades gracefully without keys: generate-candidates.mjs --dry-run needs none, optimize-prompt.mjs/export-dpo-dataset.mjs report "no preference data yet" instead of crashing, and /api/describe falls back to the client's local deterministic description (localAdc()/localAnswer() in src/app.js) when no LLM key is configured.
Key docs
SCOPE.md: scope and architecture (the original plan — some sponsor integrations described there are seams/fallbacks rather than wired-up live services; see this README for what's actually implemented)TODO.md: sprint checklistdocs/TERAC_ANNOTATION_PLAN.md: labeling plandocs/TERAC_CENTRAL_STORY.md: how Terac labels train the description selectordocs/DEVPOST_DRAFT.md: submission draft
Rebuild trigger
Analysis
View
Metric
- 57
- 57
- 36
- 5
- 3
Figures cover GitHub contributors during the hackathon window. A co-authored commit counts in full for each author, so per-member totals add up to more than the whole-team figures.
Technology
- CSSIn code
- HTMLIn code
- JavaScriptIn code
- PythonIn code
- Node.jsClaimed
- RedisClaimed
- VercelClaimed
4 of 7 appear in the indexed code. 3 claimed on Devpost could not be matched to code, which may simply mean the tool leaves no trace in the repository.
AI coding agents
- Claude CodeCommits
- CursorCommits
Detected from committed agent config files and commit authorship. Absence of a signal is not proof an agent was unused.
Codebase size
Source size
2.9 MB
Source files
81
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
bbioren/match-vision
193 files · 240.1 MB · @ 5a2d918
Structure
Interface
12 files · 6%Screens, components and styles rendered to the user.
API & routing
8 files · 4%Request entry points: routes, handlers and controllers.
Application logic
52 files · 27%Domain rules, services and shared utilities.
+3 moreData & schema
2 files · 1%Schema definitions, migrations and data access.
Supporting
Layers are inferred from where files sit in the tree, not from reading the code. A project that names its directories unconventionally will read oddly here — open the file browser to check anything the diagram implies.
Languages
- JavaScript93%
- Markdown4%
- HTML2%
- Python1%
- CSS0%
- Shell0%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
analytics/requirements.txt
pypi · 6- kloppy
- multimethod
- pandas
- scikit-learn
- socceraction
- xgboost
package.json
npm · 3- @upstash/redis
- @vercel/blob
- ioredis
Declared in the repository’s manifests at the indexed commit. A declared package is not proof it is used, and runtime dependencies are listed first.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.