"""World Cup Heros β football shot analysis (Gradio, ZeroGPU Space). One RF-DETR-Seg model detects ball + players + goal; pure geometry/physics derive the events: was it a goal (+ when), who shot it, their pose & shooting foot, and shot speed. Upload a clip or pick one of the 12 held-out test examples. ZeroGPU: only the RF-DETR detection runs inside the @spaces.GPU window; the goal physics, possession, MediaPipe pose, speed and rendering all run on CPU. Left: upload + example clips. Right: the clean analysed video (player masks + P# labels + ball trail + goal), then a Result table (goal / foot, predicted vs actual), a Shot-speed card, and a Pose-Detection card. """ import glob import json import os import gradio as gr import spaces from huggingface_hub import hf_hub_download from futheros import pipeline as P from futheros import coach as C from futheros.render import render, pose_card_img HF_MODEL = "build-small-hackathon/futheros-rfdetr-seg" # RF-DETR-Seg-Small checkpoint RENDER_DIR = "/tmp/app_renders"; os.makedirs(RENDER_DIR, exist_ok=True) GT = json.load(open("labels/goal_labels.json")) LEG = json.load(open("labels/leg_labels.json")) EXAMPLES = sorted(glob.glob("examples/*.mp4")) EDGE_EMOJIS = ["π΄", "β½", "π«π·", "β½", "π§π·", "β½", "π¦π·", "β½", "πͺπΈ", "β½", "π©πͺ", "β½", "π΅πΉ", "β½", "π³π±", "β½", "π§πͺ", "β½"] EDGE_STRIP = " \n".join(EDGE_EMOJIS * 2) CONFETTI_JS = """ (g) => { if (g === 'goal') { const fire = () => { window.confetti({particleCount:200, spread:100, origin:{y:0.6}}); setTimeout(()=>window.confetti({particleCount:150, spread:120, origin:{y:0.5}}), 200); setTimeout(()=>window.confetti({particleCount:120, angle:60, spread:80, origin:{x:0}}), 350); setTimeout(()=>window.confetti({particleCount:120, angle:120, spread:80, origin:{x:1}}), 350); }; if (!window.confetti) { const s = document.createElement('script'); s.src = 'https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js'; s.onload = fire; document.head.appendChild(s); } else { fire(); } } } """ CSS = """ .gradio-container{background:linear-gradient(160deg,#f3fdf6 0%,#eef6ff 55%,#fff7ea 100%) !important;} #wc-head{text-align:center !important;max-width:980px;margin:0 auto !important;} #wc-head h1{text-align:center !important;font-size:2.7rem !important;font-weight:900 !important; letter-spacing:-1px;margin:6px 0 4px !important; background:linear-gradient(90deg,#15803d,#0891b2,#d97706); -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;} #wc-head p,#wc-head span{text-align:center !important;color:#475569 !important;} button.primary,.primary>button{background:linear-gradient(90deg,#16a34a,#0ea5e9) !important; border:none !important;color:#fff !important;font-weight:700 !important;} .block,.gr-box{border-radius:14px !important;} footer{display:none !important;} """ _MODEL = None def _model(): """Lazy-load the detector (checkpoint pulled from HF, cached).""" global _MODEL if _MODEL is None: ckpt = hf_hub_download(HF_MODEL, "checkpoint_best_ema.pth") _MODEL = P.rfdetr_model(ckpt, "small") return _MODEL @spaces.GPU(duration=120) def _detect(video_path): """The only GPU step: RF-DETR-Seg over every frame -> tracks dict.""" return P.detect_rfdetr(video_path, _model()) def _truth(name): rel = f"5/{name}.mp4" return {"goal": GT.get(rel), "leg": LEG.get(rel)} def _table_html(res, truth): def row(metric, pred, actual, match=None): key = pred if match is None else match # color on verdict, not on display text ok = (actual is None) or (str(key).upper() == str(actual).upper()) col = "#16a34a" if ok else "#dc2626" act = f"
| METRIC | " "PREDICTED | " "ACTUAL |
|---|