Spaces:
Sleeping
Sleeping
| """Leaderboard backend for Reachy Mini Simon JS. | |
| Single endpoint: POST /score. Validates the caller's HF OAuth token, | |
| keeps one best score per (username, difficulty), persists the merged | |
| table back to the public dataset `apirrone/reachy-mini-simon-scores` | |
| as `scores.json`. | |
| Reads happen client-side directly against the dataset CDN โ no need | |
| to round-trip through here for the leaderboard render. | |
| """ | |
| import json | |
| import os | |
| import threading | |
| from datetime import datetime, timezone | |
| import httpx | |
| from fastapi import FastAPI, Header, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from huggingface_hub import HfApi | |
| from pydantic import BaseModel, Field | |
| # โโโ Config โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| DATASET_REPO = os.environ.get("DATASET_REPO", "apirrone/reachy-mini-simon-scores") | |
| SCORES_PATH = "scores.json" | |
| HF_TOKEN = os.environ.get("HF_TOKEN") # set as Space secret | |
| DATASET_URL = f"https://huggingface.co/datasets/{DATASET_REPO}/resolve/main/{SCORES_PATH}" | |
| DIFFICULTIES = {"1", "2", "3"} | |
| MAX_ENTRIES = 100 # keep table bounded | |
| # โโโ App โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| app = FastAPI(title="Reachy Mini Simon โ leaderboard") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # Spaces serve under their own origin | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| api = HfApi(token=HF_TOKEN) if HF_TOKEN else None | |
| _write_lock = threading.Lock() # serialise commits to the dataset | |
| _cache: dict = {} # latest table, freshly loaded on boot | |
| # โโโ Models โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class ScorePayload(BaseModel): | |
| difficulty: str | |
| score: int | |
| display_name: str = "" | |
| avatar_url: str = "" | |
| class ScoreEntry(BaseModel): | |
| u: str = Field(..., description="HF username (slug)") | |
| n: str = Field("", description="display name") | |
| a: str = Field("", description="avatar URL") | |
| s: int = Field(0, description="score (best round reached)") | |
| d: str = Field("", description="ISO timestamp") | |
| # โโโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _load_from_hub() -> dict: | |
| try: | |
| r = httpx.get(DATASET_URL, timeout=10.0) | |
| if r.status_code == 200: | |
| return r.json() | |
| except Exception as e: | |
| print(f"[leaderboard] could not load {DATASET_URL}: {e}") | |
| return {} | |
| def _commit(data: dict) -> None: | |
| if not api: | |
| print("[leaderboard] HF_TOKEN missing โ skipping commit") | |
| return | |
| body = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8") | |
| api.upload_file( | |
| path_or_fileobj=body, | |
| path_in_repo=SCORES_PATH, | |
| repo_id=DATASET_REPO, | |
| repo_type="dataset", | |
| commit_message="update leaderboard", | |
| ) | |
| async def _whoami(token: str) -> dict: | |
| async with httpx.AsyncClient(timeout=10.0) as client: | |
| r = await client.get( | |
| "https://huggingface.co/api/whoami-v2", | |
| headers={"Authorization": f"Bearer {token}"}, | |
| ) | |
| if r.status_code != 200: | |
| raise HTTPException(401, "Invalid HF token") | |
| return r.json() | |
| # โโโ Boot โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _boot() -> None: | |
| global _cache | |
| _cache = _load_from_hub() | |
| print(f"[leaderboard] loaded {sum(len(v) for v in _cache.values())} entries") | |
| # โโโ Routes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def root(): | |
| return {"status": "ok", "dataset": DATASET_URL} | |
| async def post_score(payload: ScorePayload, authorization: str = Header(default="")): | |
| if not authorization.startswith("Bearer "): | |
| raise HTTPException(401, "Missing Bearer token") | |
| token = authorization[len("Bearer "):].strip() | |
| if payload.difficulty not in DIFFICULTIES: | |
| raise HTTPException(400, "Invalid difficulty") | |
| if not (0 < payload.score < 1000): | |
| raise HTTPException(400, "Invalid score") | |
| me = await _whoami(token) | |
| username = me.get("name") or me.get("preferred_username") or "" | |
| if not username: | |
| raise HTTPException(401, "Cannot resolve username") | |
| with _write_lock: | |
| bucket = _cache.setdefault(payload.difficulty, []) | |
| mine = next((e for e in bucket if e.get("u") == username), None) | |
| if mine and mine["s"] >= payload.score: | |
| return {"status": "not_best", "entries": bucket} | |
| entry = { | |
| "u": username, | |
| "n": payload.display_name or username, | |
| "a": payload.avatar_url or "", | |
| "s": int(payload.score), | |
| "d": datetime.now(timezone.utc).isoformat(timespec="seconds"), | |
| } | |
| if mine: | |
| bucket.remove(mine) | |
| bucket.append(entry) | |
| bucket.sort(key=lambda e: e["s"], reverse=True) | |
| del bucket[MAX_ENTRIES:] | |
| snapshot = {k: list(v) for k, v in _cache.items()} | |
| # Commit outside the lock โ network can be slow. | |
| try: | |
| _commit(snapshot) | |
| except Exception as e: | |
| print(f"[leaderboard] commit failed: {e}") | |
| # The score is still in memory; next successful write will catch up. | |
| return {"status": "saved", "entries": _cache[payload.difficulty]} | |