尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
Explainable football match intelligence with evidence-linked insights, fan views, and broadcast overlays. Built with Microsoft Foundry and Azure.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Explainable match intelligence for broadcasters and fans, from synthetic football events. Every published line cites the events it came from.
MatchLens is for the people who put words on screen during a match: a studio editor or a stream producer who has to say why a moment matters without risking a wrong number on air. It turns a synthetic event stream into cards that explain each key moment, timed overlay cues for a broadcast graphic, a full-time recap, and two genuinely different experiences: one for a casual fan (in English, German or Ukrainian, from either club's side) and one for an analyst.
Facts are rendered from structured data, never written by a model. Three Microsoft Foundry agents work with them. The claim agent proposes what an analyst card may assert, and a deterministic validator accepts or rejects each claim. The personalizer rewrites fan cards for a language and a club. The critic reviews each rewrite for meaning. AI wording is always labelled, and the on-air graphic shows template-rendered facts only.
Built for the Microsoft × Premier League Inside the Game Developer Hackathon. Project page: https://innovationstudio.microsoft.com/hackathons/insidethegamedeveloperhackathon/project/126828
| Live demo, Azure (free, no sign-in, no API key) | https://wonderful-hill-04967dd0f.5.azurestaticapps.net/: Azure Static Web Apps with a managed Azure Functions API for live Foundry checks |
| Mirror | https://vbabenko97.github.io/matchlens-ai/: GitHub Pages, static only (no live-check button) |
| Demo video | https://youtu.be/4q2mIk0xPjs: 1 min 38 s, English captions |
| Microsoft technology | Microsoft Foundry (Azure OpenAI gpt-4.1-mini, Responses API v1, structured outputs, function calling) · Azure Static Web Apps · Azure Functions (managed API, Python 3.11) · Azure Table Storage (shared spend cap) · Application Insights (live-check telemetry) · GitHub Actions |
| Data | 100% synthetic, generated by this repository; fictional teams and players; no real match data, footage or club branding |
| Entered for | Grand Prize and Best Enterprise Solution (the deployment case) |
pip install -e . && python -m matchlens feed --seed 1 --speed 60 | python -m matchlens stream -. Events arrive one at a time; each is validated on arrival, and each card is printed when its last evidence event arrives.flowchart TD
A["<b>Ingest</b><br/>synthetic feed, validated event by event<br/>same seed, same bytes"] --> B["<b>Interpret</b><br/>metrics and key moments,<br/>each with its source event IDs"]
B --> C["<b>Explain</b><br/>cards of template-rendered facts,<br/>published at their last evidence event"]
C --> D["<b>Claim agent</b> · Microsoft Foundry<br/>read-only tools → structured claims"]
D --> E{{"Validator"}}
E -- "rejected: reasons go back once" --> D
E -- "verified, or the computed value" --> F["<b>Analyst view</b><br/>claim check, evidence inspector"]
C --> G["<b>Personalizer</b> · Microsoft Foundry<br/>rewrites for a language and a club"]
G --> H{{"Checker<br/>numbers, names, score order"}}
H -- "passed" --> I["<b>Critic</b> · Microsoft Foundry<br/>meaning, line by line"]
H -- "rejected: reasons go back once" --> G
I -- "rejected: reasons go back once" --> G
I -- "faithful" --> J["<b>Fan view</b><br/>en / de / uk, either club<br/>still rejected → the English fact"]
C --> K["<b>Render</b><br/>overlay JSON → on-air graphic<br/>template facts only, no model wording"]
C --> L["<b>Match recap</b><br/>plain text, JSON with IDs"]
On the public pages the three agents replay recorded live runs, and every build re-runs every check on them; on Azure, the claim agent also runs live on request. A PNG of this diagram is in docs/images.
Each stage is one of the five in the challenge, §4.1, followed by the code that implements it.
matchlens feed writes it one event at a time, and matchlens stream validates each event on arrival and publishes each card as its last evidence event arrives, without looking ahead.
Code: generator.py, validate.py, stream.py, DATA_MODEL.metrics.py, moments.py.story.py, cards.py.matchlens.overlays/v1, both modes); a transparent broadcast lower-third that plays them; a match recap at full time (matchlens.recap/v1) to copy as text or download with its sources; a replay UI with pitch view, timeline and scoreboard.
Code: overlays.py, recap.py, web/.narrative.py, personalize.py, web/app.js.MatchLens is entered for Best Enterprise Solution. This is the deployment case: the controls that already work, how the demo runs on Azure, and what a first pilot would add and measure.
| Risk | Control | Code |
|---|---|---|
| A model states a wrong number or name | Facts are rendered from structured data by templates. The claim agent proposes structured claims (metric, value, event IDs) that the validator accepts or rejects. A rewrite must keep every number, score pair (home first) and name of its English fact. | claims.py, agent.py, personalize.py |
| A rewrite changes the meaning | A critic agent reviews each line that passed the checker; a missing verdict is a rejection; a line still rejected shows the English fact. The critic is a model and misses errors (measured). | critic.py |
| Look-ahead and spoilers | A card is published at its last evidence event. The claim agent's tools see the match only up to that event. The recap appears only at full time. Fed one event at a time, every match gives the same cards at the same events: checked on 31 seeds in the tests and on every match at every build. | cards.py, agent.py, recap.py, stream.py |
| A broken feed | The stream processor checks each event on arrival with the event validator's rules. The first invalid event stops publishing for the rest of the stream. | validate.py, stream.py |
| AI output taken for fact | AI wording is labelled wherever it appears. The on-air graphic shows template facts only. Synthetic xG and xPass (expected pass completion: a pass's estimated chance of arriving) are labelled as heuristics. | web/ |
| No audit trail | Live runs are recorded with response IDs and token use, and every build re-checks them. Editorial rejections, from AI reviews that no human has checked, are kept with reasons in a versioned file that the build applies to the exact text. | docs/evidence/ |
| Runaway cost from a public page | The live endpoint takes a card ID only. A shared daily cap is reserved in Azure Table Storage before any model call; if the store fails, no call is made. | live.py, quota.py |
| Exposed secrets and stored data | The browser never holds a key: live checks run in an Azure Function. Every Foundry call is made with store: false. The data is synthetic, with no personal data. |
flowchart LR
U["Judge's browser<br/>no sign-in, no key"] --> S["<b>Azure Static Web Apps</b><br/>replay UI + bundle with<br/>recorded Foundry runs"]
U -- "POST card ID" --> F["<b>Azure Functions</b><br/>/api/live-check, Python 3.11<br/>30 s deadline"]
F -- "reserve today's count first (ETag)" --> T["<b>Azure Table Storage</b><br/>shared daily cap"]
F -- "Responses API v1, store: false" --> M["<b>Microsoft Foundry</b><br/>gpt-4.1-mini"]
F -- "one telemetry record<br/>per request" --> A["<b>Application Insights</b><br/>requests and traces,<br/>ready-made queries"]
G["<b>GitHub Actions</b>"] -- "build, deploy,<br/>smoke tests after deploy and daily" --> S
matchlens stream), but the web demo replays a precomputed match.ruff.azure-functions on Azure.store: false.api/).Three agents, distinct roles, one shared state.
The model proposes, the validator decides.
Readable for people, not only for engineers. Each claim check opens with a few plain sentences built from the verdicts, never by a model: what the agent proposed, what the validator rejected and why, and how the retry ended.
Failure recovery throughout. HTTP 429 waits per Retry-After, bounded network retries, a forced final answer when the tool budget runs out, and one resend when an answer is cut off. Every fallback is labelled: the page never presents a fallback as a live response.
Recorded runs as evidence.
matchlens feed | matchlens stream -); an adapter for a real provider feed is not built yet. The on-air page is a working browser source today.Two modes that differ in substance, not just in density.
| Fan view in Ukrainian, following Eastvale | Match recap at full time |
|---|---|
Accessibility target WCAG 2.2 AA (audit):
lang attributes on rewritten lines.Demo video (1 min 38 s): https://youtu.be/4q2mIk0xPjs.
MatchLens is entered for the Grand Prize and for Best Enterprise Solution; the other category prizes are listed for the judges' reference.
| Category | How MatchLens fits |
|---|---|
| Grand Prize (explainable match intelligence, narratives, personalized experiences for fans, analysts and broadcasters) | All five pipeline stages, both audiences, broadcast output, a full-time recap, multi-language storytelling (challenge §4.1 (vi)) |
| Best Enterprise Solution (entered) | Controls a broadcaster or league would need before a model writes on screen: template-rendered facts, validator- and critic-gated AI output, labelled AI wording, an audit trail of recorded runs, fail-closed spend control, and a pilot plan with stated gaps (From demo to production) |
| Best Use of Microsoft Foundry | Three Foundry agents (tools, structured outputs), recorded and live runs with response IDs, validator-gated publishing |
| Best Multi-Agent System | A claim agent, a personalizer and a critic with distinct roles, shared verified state, validator and critic handoffs, retries and labelled fallbacks |
| Best Azure Cloud Native Integration | Static Web Apps + managed Functions + Table Storage quota, CI/CD with post-deploy smoke tests |
The web demo replays a precomputed match. The stream processor (matchlens stream) does take the events one at a time from a feed and gives the same cards at the same events, checked at every build. But it runs from the command line: the web page does not run it live, and there is no adapter for a real provider's feed. Video auto-eventing, player speed and ball speed are not modelled.
The synthetic data is football-realistic in structure: 11-a-side squads with positions, possession chains, passes, pressures, tackles, shots, scores and pitch coordinates over two halves. Its dynamics are simplified (no fouls, set pieces beyond restarts, substitutions or player skill; see DATA_MODEL), and its rates are tuned only to look plausible. Synthetic xG is an uncalibrated heuristic. Synthetic xPass is built from the generator's own pass rules, so it matches the simulated outcomes by construction (within 1 % over seeds 100–129) and proves nothing about real football. The generator gives players no passing skill, so "completed minus xPass" is luck. Ball speed is not shown: event times are drawn at random, independent of distance.
The live Foundry check covers the demo match's analyst cards only. Elsewhere the demo match replays recorded runs (claim agent, personalizer and critic); the five extra matches use a deterministic claim proposer and template wording, with no AI at all.
The personalizer checker verifies numbers, score order, names, xG and script, not meaning. The critic reviews meaning but is a model too: it misses errors and raises false alarms (measured below), and it does not check tone. The page says so on every rewritten card.
Two editorial reviews of the published run (file) rejected 30 lines that passed both the checker and the critic. Both reviews were done by AI, and no human has checked either one:
Those lines show the English fact, so 50 of the 87 Ukrainian lines remain rewrites. The recorded trace itself is unchanged. No native speaker has reviewed the German or Ukrainian text.
The per-visitor limit relies on forgeable headers. The spend cap is the shared daily count.
git clone https://github.com/vbabenko97/matchlens-ai && cd matchlens-ai
python -m venv .venv && . .venv/bin/activate
pip install -e .
# What the public build serves: seed 1 (2-3) with the recorded Foundry runs, plus five extra matches
python -m matchlens serve --offline \
--agent-trace docs/evidence/agent-trace-2026-10-24.json \
--personalize-trace docs/evidence/personalize-trace-v2-2026-10-24.json \
--editorial-review docs/evidence/personalize-review-v3-2026-10-25.json
# then open http://127.0.0.1:8000/
Run it from the repository root. With --offline no Azure call is made: the agents' recorded runs are replayed and re-checked. Plain python -m matchlens serve loads no recorded run, so it shows only template wording, labelled as such, with no Language and Follow choice and no recorded claim check; without --offline it also uses Azure credentials when they are set. Options: --seed N, --file match.jsonl, --port, --offline, --single-match, --agent-trace, --personalize-trace, --editorial-review, --live-api. python -m matchlens export-site --out site/ with the same options writes the same UI as a static folder.
pip install -e ".[dev]"
python -m pytest -q && ruff check . && ruff format --check .
pip install -e ".[e2e]" && python -m playwright install chromium && python -m pytest -q -m e2e
python -m matchlens generate --seed 42 --out match.jsonl # full 90-minute synthetic match
python -m matchlens validate match.jsonl # format + invariant checks
python -m matchlens metrics match.jsonl # event-grounded metrics as JSON
python -m matchlens moments match.jsonl # card moments with evidence IDs
python -m matchlens cards match.jsonl --mode casual # verified insight cards (--json, --offline)
python -m matchlens overlays match.jsonl --out overlays.json # timed overlay cues, both modes
python -m matchlens recap match.jsonl # full-time recap as text (--json: with IDs)
python -m matchlens feed --seed 1 --speed 60 | python -m matchlens stream - # a feed, processed as it arrives
Event by event. feed writes the match record and then one event per line, flushed, waiting between events as the match clock does (--speed 60: one match minute per second; 0: no waiting). stream reads a file or standard input line by line.
EventValidator; validate_match runs the same checks over a whole match). The first invalid event stops publishing.--json gives one record per card. No model is called per event.The replay UI has a scoreboard, a pitch view of the latest ball events, a timeline with key-moment markers, and insight cards that appear as moments happen.
src/matchlens/passing.py; the player panel adds the same pass stats per player.src/matchlens/players.py with one step per event, so nothing later in the match is counted. A team's players add up to the team metrics (tested). No AI is involved.explain_review in src/matchlens/agent.py; no model writes them): what the agent proposed, what the validator rejected and why, and how the retry ended. The claim-by-claim list with reasons and response IDs is folded below, open on the card the See the AI get fact-checked shortcut opens.src/matchlens/recap.py). It adds no wording: every line is a card line with its IDs, so the recorded language and club variants apply, and lines whose rewrite was rejected stay in English. Copy recap copies plain text with a provenance line; Recap JSON (matchlens.recap-export/v1) lists each line with its language, status (verified fact, checked AI rewrite or English fact kept), event and metric IDs. It is closed again if the replay goes back before full time.Language and club. The personalizer makes one Foundry call per card with structured JSON output. check_line (src/matchlens/personalize.py) compares every rewritten line with its English fact:
xG kept, the right script, no Russian-only letters in Ukrainian, no markup.Lines that pass go to the critic agent (src/matchlens/critic.py, one call per card and attempt). A line rejected by the checker or the critic goes back once with the reasons; still rejected, the English fact is shown with lang="en".
python -m matchlens personalize-review --out <trace> (critic on; --no-critic for the personalizer alone); replay with --personalize-trace <trace> --editorial-review <review>. The published run took 90 personalizer calls and 89 critic calls (about 91,000 tokens in total).The critic, measured. Two checks, both small; neither proves the critic generalizes.
python -m matchlens critic-eval --from <file> re-scores it). The critic judged the 220 lines of the first, critic-free run that passed the checker. Claude Code's review had rejected 12 of them. The critic caught 8 (recall 0.67) and raised 13 false alarms (precision 0.38); most false alarms object to tone words, a few are plain misreadings. The prompt names categories of error only and was not tuned on these lines, so this is a regression check, not a benchmark.Claim agent. python -m matchlens agent-review --seed 1 runs one bounded Foundry agent per analyst card, with tools list_metrics, get_metric and get_events. In the recorded run (gpt-4.1-mini, 19 calls, 27 tool calls), the validator rejected a claim at 77' that cited the wrong events, and the retry was verified.
Live check (Azure build). POST /api/live-check with {"card_id": ...} only (src/matchlens/live.py, adapter in api/). The reply carries the review with response IDs, token use and latency, or a labelled fallback reason; the page keeps the recorded run whenever no live call is made. Locally: python -m matchlens serve --live-api.
On-air graphic. broadcast.html reads the overlay JSON and draws the active cue as a lower-third on a transparent page (1920×1080 or 1280×720). Only the verified "why" fact goes on air; analyst cues add source event IDs.
| URL parameter | Effect |
|---|---|
mode=casual / mode=analyst | audience mode (default casual) |
seed=<n> | another match from the picker |
bg=transparent / green / dark | compositing (default), chroma green, or preview |
cue=<cue_id> | show one cue and hold it |
rundown=1&hold=8 | play every cue in turn, looping |
from=<s>&speed=<x> | follow the match clock; a cue never shows before its start_s |
More matches. The demo match is seed 1, the only one with the recorded and live Foundry runs. The picker adds five untuned seeds from the checked range 100–129, labelled by seed only so the picker never spoils a result: 121 (a 3–4 comeback), 106 (3–3), 103 (4–1 comeback), 123 (0–5) and 124 (0–1). They are built offline, and the page says so.
Export the three variables from .env.example; the CLI reads the process environment and does not load .env files.
export AZURE_OPENAI_ENDPOINT=https://<resource>.services.ai.azure.com
export AZURE_OPENAI_DEPLOYMENT=<deployment-name> # sent as "model"
read -rs AZURE_OPENAI_API_KEY && export AZURE_OPENAI_API_KEY # never commit
python -m matchlens azure-check # exactly one live call, no fallback, redacted report
Calls go to POST {endpoint}/openai/v1/responses with store: false. Without credentials, or on any failure, card wording falls back to a template labelled "Offline fallback: deterministic template wording — not an Azure response." Verified facts are always rendered from structured data, never from model output. Steps and evidence: AZURE_VERIFICATION, DEPLOYMENT.
| Path | Contents |
|---|---|
src/matchlens/ | generator, validator, metrics, moments, story, cards, overlays, the three agents, live endpoint, quota, replay bundle, static export |
src/matchlens/web/ | replay UI (index.html, app.js) and on-air graphic (broadcast.*) |
api/ | Azure Functions adapter for /api/live-check, and its logging levels (host.json) |
tests/ | unit and Playwright end-to-end tests |
docs/ | SPEC, TASKS (verified checkpoints), DATA_MODEL, DEPLOYMENT, AZURE_VERIFICATION, ACCESSIBILITY, SUBMISSION (form texts), VIDEO_SCRIPT, telemetry queries, evidence/ |
Original code written for this hackathon with Claude Code as the coding assistant (CLAUDE.md holds the project rules). Only original synthetic data and code are included. No secrets are committed. Azure runtime costs are separate from any coding-assistant usage.
MIT, see LICENSE. No runtime dependencies besides azure-functions for the Azure adapter. The development tools (pytest, ruff, Playwright, setuptools) are MIT, BSD or Apache licensed and are not redistributed.
live.py, api/ |
| Silent breakage | Playwright smoke tests run after every deploy and daily on both hosts through the end of judging. They never press the paid live-check button, so the live call is checked by hand. | .github/workflows/ |
| No view of the live endpoint in use | Each live-check request, refused or not, writes one telemetry record: outcome and fallback reason, timing, model calls, tokens and HTTP 429 answers (also for a run that fails half-way), claims rejected per attempt and response IDs. The Functions host sends it to Application Insights (traces), connected on the Azure host since 2026-10-25 (screenshot); server-side failures log at Warning, so an alert can watch them. No key, client address or request text is logged. Ready-made queries: telemetry.kql. | live.py, api/host.json |
Microsoft Agent Framework, MCP and Fabric are not used; the agents call the Responses API directly.
seed, mode, t (match time in seconds), speed, lang, follow, player.