0xpaona's picture
Upload folder using huggingface_hub
cb414cf verified
"""Offsides — Tactical Edge Detection Demo.
Gradio app displaying pre-computed Qwen-VL 72B tactical assessments
of UEFA Champions League matches on AMD MI300X.
"""
import json
from pathlib import Path
import gradio as gr
import plotly.graph_objects as go
APP_DIR = Path(__file__).resolve().parent
RESULTS_PATH = APP_DIR / "data" / "vlm_results" / "results.json"
DEMO_PATH = APP_DIR / "data" / "demo_matches.json"
FRAMES_DIR = APP_DIR / "data" / "vlm_results" / "frames"
def load_results():
with open(RESULTS_PATH) as f:
results = json.load(f)
with open(DEMO_PATH) as f:
demos = json.load(f)
demo_lookup = {d["match_id"]: d for d in demos}
for m in results["matches"]:
demo = demo_lookup.get(m["match_id"], {})
m["first_leg"] = demo.get("first_leg", "")
m["odds"] = demo.get("odds", {})
m["narrative"] = demo.get("narrative", "")
return results
RESULTS = load_results()
MATCHES = RESULTS["matches"]
def result_key(actual_result: str) -> str:
if actual_result == "home_win":
return "home"
if actual_result == "away_win":
return "away"
return "draw"
def get_match_choices():
choices = []
for m in MATCHES:
label = f"{m['home_team']} vs {m['away_team']}{m['stage']} ({m['date']})"
choices.append(label)
return choices
def get_scorecard():
correct = 0
for m in MATCHES:
edge = m["vlm_assessment"]["edge"]
actual = result_key(m["actual_result"])
best = max(edge.items(), key=lambda x: x[1])
if best[0] == actual:
correct += 1
return correct, len(MATCHES)
def make_prob_chart(match):
market = match["market_odds"]
vlm = match["vlm_assessment"]["probabilities"]
categories = ["Home", "Draw", "Away"]
market_vals = [market["home"] * 100, market["draw"] * 100, market["away"] * 100]
vlm_vals = [vlm.get("home", 0) * 100, vlm.get("draw", 0) * 100, vlm.get("away", 0) * 100]
fig = go.Figure()
fig.add_trace(go.Bar(
name="Market Implied",
x=categories,
y=market_vals,
marker_color="#6366f1",
text=[f"{v:.0f}%" for v in market_vals],
textposition="outside",
))
fig.add_trace(go.Bar(
name="VLM Assessment",
x=categories,
y=vlm_vals,
marker_color="#10b981",
text=[f"{v:.0f}%" for v in vlm_vals],
textposition="outside",
))
fig.update_layout(
barmode="group",
title="Probability Comparison: Market vs VLM",
yaxis_title="Probability (%)",
yaxis_range=[0, 75],
template="plotly_dark",
height=350,
margin=dict(t=40, b=40),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
)
return fig
def get_frame_images(match):
images = []
for fp in match.get("frames_used", []):
parts = Path(fp).parts
match_dir = parts[2]
frame_name = parts[-1]
local_path = FRAMES_DIR / match_dir / frame_name
if local_path.exists():
images.append(str(local_path))
return images
def format_edge_badge(match):
edge = match["vlm_assessment"]["edge"]
actual = result_key(match["actual_result"])
best = max(edge.items(), key=lambda x: x[1])
best_outcome, best_val = best
correct = best_outcome == actual
outcome_label = {"home": match["home_team"], "draw": "Draw", "away": match["away_team"]}
badge = f"**Edge: +{best_val*100:.0f}pp on {outcome_label[best_outcome]}**"
if correct:
return f"### {badge}\n\nActual result: **{match['actual_score']}** ({match['actual_result'].replace('_', ' ')}) — CORRECT"
else:
return f"### {badge}\n\nActual result: **{match['actual_score']}** ({match['actual_result'].replace('_', ' ')})"
def format_reasoning(match):
a = match["vlm_assessment"]
lines = []
lines.append(f"**Confidence:** {a['confidence']}")
lines.append("")
lines.append(f"**Reasoning:** {a['reasoning']}")
lines.append("")
lines.append("**Visual Evidence:**")
for ev in a.get("visual_evidence", []):
lines.append(f"- {ev}")
lines.append("")
lines.append(f"**Edge Signal:** {a['edge_signal']}")
return "\n".join(lines)
def format_metrics(match):
ctx = match.get("metrics_context", {})
lines = []
for side, label in [("home", match["home_team"]), ("away", match["away_team"])]:
data = ctx.get(side, {})
metrics = data.get("metrics", {})
if not metrics:
continue
lines.append(f"**{label}** (last 3 matches):")
matches_analyzed = data.get("matches_analyzed", [])
if matches_analyzed:
lines.append(f"- Matches: {', '.join(m.replace('_', ' ') for m in matches_analyzed)}")
if "avg_pressing_speed" in metrics:
lines.append(f"- Pressing speed: {metrics['avg_pressing_speed']:.4f}")
if "avg_def_line_movement" in metrics:
lines.append(f"- Defensive line movement: {metrics['avg_def_line_movement']:.4f}")
if "avg_compactness_delta" in metrics:
lines.append(f"- Compactness delta: {metrics['avg_compactness_delta']:.3f}")
if "avg_transition_speed" in metrics:
lines.append(f"- Transition speed: {metrics['avg_transition_speed']:.4f}")
lines.append("")
return "\n".join(lines)
def format_stats(match):
stats = match.get("stats", {})
lines = []
for side in ["home", "away"]:
s = stats.get(side, {})
if not s:
continue
lines.append(f"**{s.get('team', side.title())}:**")
lines.append(f"| Metric | Value |")
lines.append(f"|--------|-------|")
lines.append(f"| xG/match | {s.get('xg_last5', '-')} |")
lines.append(f"| xGA/match | {s.get('xga_last5', '-')} |")
lines.append(f"| PPDA | {s.get('ppda', '-')} |")
lines.append(f"| Possession | {s.get('possession_pct', '-')}% |")
lines.append(f"| Form | {s.get('form', '-')} |")
lines.append(f"| Goals (last 5) | {s.get('goals_scored_last5', '-')}F / {s.get('goals_conceded_last5', '-')}A |")
lines.append("")
return "\n".join(lines)
def format_match_info(match):
lines = []
lines.append(f"**{match['home_team']}** vs **{match['away_team']}**")
lines.append(f"- Stage: {match['stage']}")
lines.append(f"- Date: {match['date']}")
if match.get("first_leg"):
lines.append(f"- First leg: {match['first_leg']}")
odds = match.get("odds", {})
if odds:
lines.append(f"- Decimal odds: {match['home_team']} {odds.get('home', '-')} / Draw {odds.get('draw', '-')} / {match['away_team']} {odds.get('away', '-')}")
market = match["market_odds"]
lines.append(f"- Implied probability: {match['home_team']} {market['home']*100:.0f}% / Draw {market['draw']*100:.0f}% / {match['away_team']} {market['away']*100:.0f}%")
if match.get("narrative"):
lines.append(f"\n*{match['narrative']}*")
return "\n".join(lines)
def on_match_select(choice):
idx = get_match_choices().index(choice)
match = MATCHES[idx]
chart = make_prob_chart(match)
frames = get_frame_images(match)
edge_text = format_edge_badge(match)
reasoning_text = format_reasoning(match)
metrics_text = format_metrics(match)
stats_text = format_stats(match)
info_text = format_match_info(match)
return chart, frames, edge_text, reasoning_text, metrics_text, stats_text, info_text
correct, total = get_scorecard()
with gr.Blocks(title="Offsides — Tactical Edge Detection") as demo:
gr.Markdown(f"""
# Offsides — Tactical Edge Detection
**Where the market gets it wrong.** Multimodal AI analyzes UEFA Champions League footage using YOLO + Qwen-VL 72B on AMD MI300X to detect mispriced prediction markets.
**Scorecard: {correct}/{total} correct edge calls** | Model: {RESULTS['model']} | Generated: {RESULTS['generated_at'][:10]}
""")
with gr.Row():
match_dropdown = gr.Dropdown(
choices=get_match_choices(),
value=get_match_choices()[0],
label="Select Match",
interactive=True,
)
with gr.Row():
with gr.Column(scale=1):
prob_chart = gr.Plot(label="Probability Comparison")
edge_badge = gr.Markdown()
reasoning_box = gr.Markdown(label="VLM Assessment")
with gr.Column(scale=1):
frame_gallery = gr.Gallery(
label="Annotated Frames (analyzed by VLM)",
columns=2,
height=400,
)
with gr.Accordion("Tactical Metrics", open=False):
metrics_box = gr.Markdown()
with gr.Accordion("Match Statistics", open=False):
stats_box = gr.Markdown()
with gr.Row():
info_box = gr.Markdown()
match_dropdown.change(
fn=on_match_select,
inputs=[match_dropdown],
outputs=[prob_chart, frame_gallery, edge_badge, reasoning_box, metrics_box, stats_box, info_box],
)
demo.load(
fn=on_match_select,
inputs=[match_dropdown],
outputs=[prob_chart, frame_gallery, edge_badge, reasoning_box, metrics_box, stats_box, info_box],
)
gr.Markdown("""
---
**Architecture:** YouTube highlights → Frame extraction → YOLO detection → Annotation (OpenCV) → Qwen-VL 72B reasoning (AMD MI300X via vLLM on ROCm)
**How it works:** For each upcoming match, the system analyzes the most recent 3 matches for both teams. YOLO detects player positions and ball location. OpenCV renders tactical overlays (defensive lines, compactness ellipses, team colors). Qwen-VL reasons over these annotated frames alongside stats and market odds to identify where the market may be mispriced.
Built for the AMD Developer Hackathon 2026 (Track 3: Vision & Multimodal AI)
""")
if __name__ == "__main__":
demo.launch()