File size: 6,474 Bytes
9374e3e c52625f 9374e3e c52625f 9374e3e c52625f 9374e3e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | """AI VAR β Gradio UI (HF Spaces entrypoint).
Upload one or more camera angles (videos β€2 min and/or images); each file is
treated as a separate view and fused SoccerNet-VARS style.
"""
import traceback
from pathlib import Path
import gradio as gr
import spaces
from aivar import cleanup
from aivar.ingest import IngestError
from aivar.llm import BudgetExceeded
from aivar.pipeline import analyze
from aivar.ratelimit import RateLimited
from aivar.schemas import AnalysisResult, Decision, EvidenceStatus
cleanup.start_sweeper()
@spaces.GPU(duration=90)
def gpu_vision(views):
"""Runs the YOLO+ByteTrack stage on ZeroGPU-allocated hardware."""
from aivar import vision
for v in views:
vision.analyze_view(v)
return views
STATUS_ICON = {EvidenceStatus.CONFIRMED: "β
",
EvidenceStatus.CONTRADICTED: "β",
EvidenceStatus.NOT_VISIBLE: "πΆοΈ"}
DECISION_COLOR = {Decision.RED_CARD: "#c62828", Decision.YELLOW_CARD: "#f9a825",
Decision.PENALTY: "#6a1b9a", Decision.INSUFFICIENT_EVIDENCE: "#546e7a"}
def _verdict_md(r: AnalysisResult) -> str:
v = r.verdict
color = DECISION_COLOR.get(v.decision, "#2e7d32")
cached = " Β· β‘ from cache (0 Gemini calls)" if r.from_cache else f" Β· {r.gemini_calls} Gemini calls"
lines = [
f"## <span style='color:{color}'>{v.decision.value}</span>",
f"**Incident:** {v.incident.value} Β· **Confidence:** {v.confidence}%{cached}",
]
inc = r.incident
if any([inc.offending_team, inc.offending_player, inc.fouled_team, inc.fouled_player]):
foul_by = " ".join(p for p in [inc.offending_team, inc.offending_player] if p) or "unknown"
on = " ".join(p for p in [inc.fouled_team, inc.fouled_player] if p) or "unknown"
lines.append(f"**Foul by:** {foul_by} Β· **On:** {on}")
lines += ["", f"**Why:** {v.why}"]
if v.why_not:
lines += ["", f"**Why not:** {v.why_not}"]
if v.rule_citations:
lines += ["", "### π Rule citations (IFAB Laws of the Game 2025/26)"]
for c in v.rule_citations:
lines.append(f"> **{c.law} β {c.section}**: β{c.quote}β")
if v.missing_evidence:
lines += ["", "### π Missing evidence"]
lines += [f"- {m}" for m in v.missing_evidence]
if v.recommendation:
lines += ["", f"**Recommendation:** {v.recommendation}"]
return "\n".join(lines)
def _evidence_md(r: AnalysisResult) -> str:
if not r.evidence:
return "_No checklist evaluated._"
lines = ["### Evidence checklist (fused across angles)"]
for e in r.evidence:
icon = STATUS_ICON[e.status]
src = f" β via Angle {e.source_angle}" if e.source_angle else ""
crit = " **[critical]**" if e.critical else ""
conf = f" ({e.confidence}%)" if e.confidence else ""
lines.append(f"- {icon} **{e.question}**{crit}{src}{conf} \n {e.detail}")
if e.conflict:
lines.append(" β οΈ *Angles disagree on this item.*")
return "\n".join(lines)
def preview_files(files):
if not files:
return []
paths = [f.name if hasattr(f, "name") else f for f in files]
return [(p, f"Angle {i}") for i, p in enumerate(paths, start=1)]
def run(files, user_key, progress=gr.Progress()):
if not files:
raise gr.Error("Upload at least one video (β€2 min) or image.")
paths = [f.name if hasattr(f, "name") else f for f in files]
api_key = (user_key or "").strip() or None
try:
result = analyze(paths, progress=lambda m: progress(0, desc=m), api_key=api_key,
vision_fn=gpu_vision)
except (IngestError, BudgetExceeded, RateLimited) as e:
raise gr.Error(str(e))
except Exception as e:
traceback.print_exc()
raise gr.Error(f"Analysis failed: {e}")
gallery = []
for view in result.views:
for kf in view.keyframes:
if not Path(kf.path).exists():
continue # swept by the frame-cleanup TTL; cache hit still shows verdict/evidence
tag = f"Angle {view.angle_id} @ {kf.timestamp:.2f}s"
if kf.is_replay:
tag += " (replay)"
gallery.append((kf.path, tag))
return _verdict_md(result), _evidence_md(result), gallery
with gr.Blocks(title="AI VAR β Football Referee Assistant") as demo:
gr.Markdown(
"# β½ AI VAR β Intelligent Referee Decision Assistant\n"
"Upload **multiple camera angles** β videos (β€2 min) and/or photos of the same "
"incident. Decisions are grounded in the **IFAB Laws of the Game 2025/26** and "
"the system refuses to guess when evidence is insufficient.")
with gr.Row():
with gr.Column(scale=1):
files = gr.File(label="Camera angles (videos / images)",
file_count="multiple",
file_types=[".mp4", ".mov", ".avi", ".mkv", ".webm",
".jpg", ".jpeg", ".png", ".webp"])
preview_out = gr.Gallery(label="Preview β uploaded angles", columns=3, height=240)
user_key = gr.Textbox(
label="Your Gemini API key (only needed after the daily free limit)",
type="password", placeholder="AIzaβ¦")
gr.Markdown("*Free tier: 25 analyses/day globally. After that, paste your "
"own key β it is used only for your request and never stored.*")
btn = gr.Button("π Analyze incident", variant="primary")
gr.Markdown("*Repeat uploads of the same footage β even re-encoded or "
"trimmed β are served instantly from the perceptual cache. "
"Extracted keyframe images are auto-deleted 10 minutes after "
"creation for privacy/disk hygiene β cached verdicts stay "
"instant, but keyframe thumbnails may no longer display.*")
with gr.Column(scale=2):
verdict_out = gr.Markdown(label="Verdict")
evidence_out = gr.Markdown(label="Evidence")
gallery_out = gr.Gallery(label="Annotated keyframes by angle", columns=6, height=260)
files.change(preview_files, inputs=[files], outputs=[preview_out])
btn.click(run, inputs=[files, user_key], outputs=[verdict_out, evidence_out, gallery_out])
if __name__ == "__main__":
demo.launch()
|