| |
| """Open Materials Challenge โ ์ ์์ ์์ํ. |
| |
| **์ด ์๋น์ค๊ฐ ํ์ง ์๋ ์ผ**: ์ฑ์ . ์คํ์ด์ค์๋ Materials Project ์๋ฃ๋ GPU ๋ ์๋ค. |
| ์ ์ถ์ ๋ฐ์ ์์ฅ(๋น๊ณต๊ฐ ๋ฐ์ดํฐ์
)์ ์ ๊ณ , ์์ปค๊ฐ ์ฑ์ ํด ๋ฃ์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ค ๋ฟ์ด๋ค. |
| ์ฌ๊ธฐ์ ๋ฌผ์ฑ์ ํ์ ํ๋ ์ฒํ๋ฉด ์ฐธ๊ฐ์๋ ๊ทผ๊ฑฐ ์๋ ๋ฐ๋ ค๋ฅผ ๋ฐ๊ฒ ๋๋ค. |
| |
| ์์ํ๋ TTL ๋น ํ ๋ฒ๋ง ๋ง๋ค์ด ๋ฏธ๋ฆฌ ์์ถํด ๋๊ณ , ์บ์๋ ๋จ์ผ ๊ฐฑ์ ์๋ก ๋๋ฆฐ๋ค. |
| ์ฒซ ํ์ด์ง๋ async ๋ก ์๋นํ๊ณ , ํ๋ 500 ํ์ฉ ๋๋ ๊ทธ๋ฆฐ๋ค. |
| """ |
| import base64 |
| import gzip |
| import hashlib |
| import hmac |
| import json |
| import os |
| import re |
| import secrets |
| import time |
| import urllib.parse |
| import urllib.request |
| import uuid |
|
|
| from fastapi import FastAPI, HTTPException, Request |
| from fastapi.middleware.gzip import GZipMiddleware |
| from fastapi.responses import (FileResponse, JSONResponse, RedirectResponse, |
| Response) |
| from pydantic import BaseModel |
|
|
| import gates |
| import seasons |
| import store |
|
|
| HERE = os.path.dirname(os.path.abspath(__file__)) |
| DATA = os.path.join(HERE, "data") |
| |
| CACHE = store.Cached(ttl=int(os.environ.get("OMC_CACHE_TTL", "60"))) |
| SALT = os.environ.get("OMC_SALT", "omc-season1") |
|
|
| OAUTH_ID = os.environ.get("OAUTH_CLIENT_ID", "") |
| OAUTH_SECRET = os.environ.get("OAUTH_CLIENT_SECRET", "") |
| OAUTH_ISS = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co") |
| SPACE_HOST = os.environ.get("SPACE_HOST", "") |
| COOKIE = "omc_session" |
| IN_FRAME = bool(SPACE_HOST) |
| COOKIE_KW = ({"samesite": "none", "secure": True} if IN_FRAME |
| else {"samesite": "lax", "secure": False}) |
| SESSION_KEY = os.environ.get("OMC_SESSION_KEY") or secrets.token_hex(16) |
|
|
| DAILY_CAP = int(os.environ.get("OMC_DAILY_CAP", "30")) |
| CAP_TZ_OFFSET = 9 * 3600 |
|
|
| app = FastAPI(title="Open Materials Challenge") |
| app.add_middleware(GZipMiddleware, minimum_size=1024) |
|
|
|
|
| class Submission(BaseModel): |
| formula: str |
| display_name: str |
| model_name: str = "" |
| rationale: str = "" |
| structure: str = "" |
| season: int = 0 |
| |
| |
| visibility: str = "private" |
|
|
|
|
| |
| def _submissions(s): |
| return CACHE.get("ids:%d" % s["number"], |
| lambda: store.listdir(seasons.path(s, "submissions"))) |
|
|
|
|
| def _board(s): |
| return CACHE.get("board:%d" % s["number"], |
| lambda: store.read(seasons.path(s, "leaderboard.json"), |
| default={}) or {}) |
|
|
|
|
| def public_id(key): |
| return "OMC-" + hashlib.sha256((SALT + key).encode()).hexdigest()[:10].upper() |
|
|
|
|
| |
| |
| |
| |
| _SAFE_KEYS = ( |
| "candidate_id", "display_name", "hf_user", "model_name", "visibility", |
| "submitted_at", "season", "scored_at", "status", "gate", "hold_kind", |
| "axes", "detail", "notes", "note", "total", "total_max", "held_axes", |
| "known_in_mp", "is_anchor", "tier", "rank", "div_rank", "division", |
| "track", "track_rank", |
| ) |
|
|
| |
| _PUBLIC_ONLY = ("composition", "measured", "n_atoms", "structure", "rationale") |
|
|
| |
| |
| _MASK_DROP = _PUBLIC_ONLY |
|
|
|
|
| def mask(rec): |
| """๊ณต๊ฐ ํ๊ฐ ๋ณผ ์ ์๋ ๊ฒ๋ง ๋จ๊ธด๋ค. |
| |
| ๋น๊ณต๊ฐ ์ ์ถ์์๋ **์์ ์ข
๋ฅ๊น์ง๋ง** ๋ณด์ฌ์ค๋ค. ์ด๋ค ํํ๊ณ๊ฐ ์์์ธ์ง๋ ๋ํ์ |
| ๊ณต๊ฐ ์ง์์ด์ด์ผ ํ์ง๋ง, ์กฐ์ฑ๋น๋ ์ ์ถ์์ ๊ฒ์ด๋ค. |
| |
| ์กฐ์ฑ์ ๊ทธ๋๋ก ๋จ๊ธฐ๋ฉด ์ ๋๋ ๊ฒ์ ๋ฌผ๋ก ์ด๊ณ , ๊ณ์ฐ๊ฐ(์์ฑ์๋์ง ๋ฑ)๋ ๋จ๊ธฐ๋ฉด ์ ๋๋ค - |
| ์์ ์งํฉ๊ณผ ํจ๊ป ๋๋ฉด ํ๋ณด ์กฐ์ฑ์ ํ์ด ๋ง์ถ ์ ์์ด ์ฌ์ค์ ์กฐ์ฑ์ ์๋ ค์ฃผ๋ ๊ฒ๊ณผ ๊ฐ๋ค. |
| """ |
| pub = rec.get("visibility") == "public" |
| comp = rec.get("composition") or {} |
| out = {k: rec.get(k) for k in _SAFE_KEYS if k in rec} |
| |
| out["division"] = division_of(comp) |
| if pub: |
| out["formula"] = rec.get("formula") |
| for k in _PUBLIC_ONLY: |
| if k in rec: |
| out[k] = rec[k] |
| out["masked"] = False |
| else: |
| out["formula"] = "ยท".join(sorted(comp)) if comp else "๋น๊ณต๊ฐ" |
| out["masked"] = True |
| return out |
|
|
|
|
| def strip_masked(entries): |
| """๋ง์คํน๋ ํญ๋ชฉ์์ ์กฐ์ฑ์ ๋๋๋ฆด ์ ์๋ ํ๋๋ฅผ ์ ๋ถ ์์ค๋ค.""" |
| for e in entries: |
| if e.get("masked"): |
| for k in _MASK_DROP: |
| e.pop(k, None) |
| return entries |
|
|
|
|
| |
| def _sign(payload): |
| raw = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("=") |
| sig = hmac.new(SESSION_KEY.encode(), raw.encode(), hashlib.sha256).hexdigest()[:32] |
| return raw + "." + sig |
|
|
|
|
| def _unsign(token): |
| try: |
| raw, sig = (token or "").rsplit(".", 1) |
| except ValueError: |
| return None |
| good = hmac.new(SESSION_KEY.encode(), raw.encode(), hashlib.sha256).hexdigest()[:32] |
| if not hmac.compare_digest(sig, good): |
| return None |
| pad = "=" * (-len(raw) % 4) |
| try: |
| return json.loads(base64.urlsafe_b64decode(raw + pad)) |
| except Exception: |
| return None |
|
|
|
|
| def current_user(request: Request): |
| return _unsign(request.cookies.get(COOKIE)) |
|
|
|
|
| def _redirect_uri(request: Request): |
| if SPACE_HOST: |
| return "https://%s/auth/callback" % SPACE_HOST |
| return str(request.base_url).rstrip("/") + "/auth/callback" |
|
|
|
|
| |
| @app.get("/") |
| async def index(): |
| |
| return FileResponse(os.path.join(HERE, "index.html")) |
|
|
|
|
| @app.get("/api/me") |
| def me(request: Request): |
| u = current_user(request) |
| return {"signed_in": bool(u), "user": u, |
| "oauth_configured": bool(OAUTH_ID and OAUTH_SECRET)} |
|
|
|
|
| @app.get("/login") |
| def login(request: Request): |
| if not (OAUTH_ID and OAUTH_SECRET): |
| raise HTTPException(503, "OAuth ๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.") |
| state = secrets.token_urlsafe(16) |
| q = urllib.parse.urlencode({ |
| "client_id": OAUTH_ID, "redirect_uri": _redirect_uri(request), |
| "response_type": "code", "scope": "openid profile", "state": state}) |
| r = RedirectResponse("%s/oauth/authorize?%s" % (OAUTH_ISS, q)) |
| r.set_cookie("omc_state", state, max_age=600, httponly=True, **COOKIE_KW) |
| return r |
|
|
|
|
| @app.get("/auth/callback") |
| def callback(request: Request, code: str = "", state: str = ""): |
| if not code or state != request.cookies.get("omc_state"): |
| return RedirectResponse("/?auth=failed") |
| body = urllib.parse.urlencode({ |
| "client_id": OAUTH_ID, "client_secret": OAUTH_SECRET, |
| "grant_type": "authorization_code", "code": code, |
| "redirect_uri": _redirect_uri(request)}).encode() |
| try: |
| req = urllib.request.Request("%s/oauth/token" % OAUTH_ISS, data=body) |
| with urllib.request.urlopen(req, timeout=30) as x: |
| tok = json.loads(x.read()) |
| req = urllib.request.Request( |
| "%s/oauth/userinfo" % OAUTH_ISS, |
| headers={"Authorization": "Bearer " + tok["access_token"]}) |
| with urllib.request.urlopen(req, timeout=30) as x: |
| info = json.loads(x.read()) |
| except Exception: |
| return RedirectResponse("/?auth=failed") |
| u = {"sub": info.get("sub"), "name": info.get("preferred_username") or info.get("name"), |
| "picture": info.get("picture")} |
| r = RedirectResponse("/?auth=ok") |
| r.set_cookie(COOKIE, _sign(u), max_age=30 * 86400, httponly=True, **COOKIE_KW) |
| r.delete_cookie("omc_state") |
| return r |
|
|
|
|
| @app.get("/logout") |
| def logout(): |
| r = RedirectResponse("/") |
| r.delete_cookie(COOKIE) |
| return r |
|
|
|
|
| @app.get("/api/seasons") |
| def season_list(): |
| return {"seasons": seasons.all_public()} |
|
|
|
|
| @app.get("/api/season") |
| def season(request: Request): |
| return seasons.public(seasons.get(request.query_params.get("season"))) |
|
|
|
|
| |
| @app.post("/api/submit") |
| def submit(s: Submission, request: Request): |
| season = seasons.get(s.season or None) |
| if not season.get("open"): |
| raise HTTPException(400, "์ด ์์ฆ์ ์ ์ถ์ ๋ฐ์ง ์์ต๋๋ค.") |
| user = current_user(request) |
| if (OAUTH_ID and OAUTH_SECRET) and not user: |
| raise HTTPException(401, "์ ์ถํ๋ ค๋ฉด Hugging Face ๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.") |
|
|
| g = seasons.gate(season) |
| ok, why, info = gates.check(s.formula, cif=s.structure or None, |
| require_elements=g.get("require_elements", ("Li",)), |
| max_atoms=g.get("max_atoms", 60)) |
| if not ok: |
| raise HTTPException(400, why) |
|
|
| comp = info["composition"] |
| |
| |
| red = info.get("reduced") or comp |
| key = "-".join("%s%g" % (k, red[k]) for k in sorted(red)) |
| cid = public_id(key) |
|
|
| ids = _submissions(season) |
| if cid in ids: |
| raise HTTPException(409, "์ด๋ฏธ ์ ์ถ๋ ์กฐ์ฑ์
๋๋ค (%s)." % cid) |
|
|
| if user: |
| today = int((time.time() + CAP_TZ_OFFSET) // 86400) |
| mine = CACHE.get("cap:%s:%d:%d" % (user.get("sub"), season["number"], today), |
| lambda: 0) |
| if mine >= DAILY_CAP: |
| raise HTTPException(429, "ํ๋ฃจ ์ ์ถ ์ํ(%d๊ฑด)์ ๋๋ฌํ์ต๋๋ค." % DAILY_CAP) |
|
|
| rec = {"candidate_id": cid, "formula": s.formula.strip(), |
| "composition": comp, "n_atoms": info["n_atoms"], |
| "display_name": (s.display_name or "").strip()[:60], |
| "model_name": (s.model_name or "").strip()[:80], |
| "rationale": (s.rationale or "").strip()[:1000], |
| "visibility": "public" if s.visibility == "public" else "private", |
| "has_structure": bool(s.structure), |
| "hf_user": (user or {}).get("name", ""), |
| "hf_sub": (user or {}).get("sub", ""), |
| "season": season["number"], |
| "submitted_at": int(time.time()), |
| "uuid": uuid.uuid4().hex} |
| store.write(seasons.path(season, "submissions/%s.json" % cid), rec, |
| summary="submit %s" % cid) |
| if s.structure: |
| |
| store.write(seasons.path(season, "structures/%s.json" % cid), |
| {"candidate_id": cid, "cif": s.structure[:400_000]}, |
| summary="structure %s" % cid) |
| CACHE.drop("ids:%d" % season["number"]) |
|
|
| scored = set((_board(season).get("entries") or {}).keys()) |
| ahead = len([i for i in _submissions(season) if i not in scored]) |
| return {"accepted": True, "candidate_id": cid, "queue_position": ahead, |
| "note": "์ฑ์ ์ ๋ณ๋ ๊ณ์ฐ ์์
์ผ๋ก ์ฒ๋ฆฌ๋๋ฉฐ ์๋ฃ๊น์ง ์๊ฐ์ด ๊ฑธ๋ฆฝ๋๋ค."} |
|
|
|
|
| |
| def _anchors(season): |
| p = os.path.join(DATA, season.get("anchors") or "") |
| if not os.path.exists(p): |
| return [] |
| out = [] |
| for a in json.load(open(p, encoding="utf-8")): |
| if not a.get("admitted"): |
| continue |
| out.append({"candidate_id": a["label"], "is_anchor": True, |
| "display_name": "๊ธฐ์ค๋ฌผ์ง", "model_name": "", |
| "formula": a.get("formula", ""), |
| "total": a.get("total"), "total_max": a.get("total_max"), |
| "tier": a.get("tier", 1), |
| "axes": {k: v["points"] for k, v in (a.get("axes") or {}).items()}, |
| "detail": {k: v["detail"] for k, v in (a.get("axes") or {}).items()}, |
| "division": _division_from_formula(a.get("formula", "")), |
| "note": a.get("note", "")}) |
| return out |
|
|
|
|
| def _division_from_formula(f): |
| """๊ธฐ์ค๋ฌผ์ง์๋ ์กฐ์ฑ ์ฌ์ ์ด ์๊ณ ํํ์ ๋ฌธ์์ด๋ง ์๋ค. |
| |
| ์ง์ ํ์ฑํ์ง ์๋๋ค - Li10Ge(PS6)2 ๊ฐ์ ๊ดํธ ํ๊ธฐ์์ ์์งํ ์ ๊ท์์ P1S6 ์ |
| ๋ด๋๋๋ค. ๊ทธ๋ฌ๋ฉด ์ด ๋ฌผ์ง์ด ํฉํ๋ฌผ์ด ์๋ ๊ฒ์ผ๋ก ๋ถ๋ฅ๋๋ค. |
| """ |
| if not f: |
| return "other" |
| try: |
| from pymatgen.core import Composition |
| return division_of({str(el): n for el, n |
| in Composition(f).get_el_amt_dict().items()}) |
| except Exception: |
| return "other" |
|
|
|
|
| |
| |
| _ANION_ORDER = ["F", "Cl", "Br", "I", "S", "Se", "O", "N", "P", "H"] |
| DIVISIONS = {"F": "fluoride", "Cl": "halide", "Br": "halide", "I": "halide", |
| "S": "sulfide", "Se": "sulfide", "O": "oxide", "N": "nitride", |
| "P": "oxide", "H": "hydride"} |
|
|
|
|
| def division_of(comp): |
| """๊ณจ๊ฒฉ ์์ด์จ์ผ๋ก ๋ถ๋ฌธ์ ์ ํ๋ค. |
| |
| ์์ฌ ์์ผ๋ฉด **๊ฐ์ฅ ๋ง์ ๊ฒ**์ ๊ณ ๋ฅธ๋ค. Li6PS5Cl ์ S ๊ฐ 5, Cl ์ด 1 ์ด๋ฏ๋ก ํฉํ๋ฌผ์ด๊ณ , |
| ์ค์ ๋ก ์์ง๋ก๋ค์ดํธ๋ ํฉํ๋ฌผ๋ก ๋ถ๋ฅ๋๋ค. ๊ฐ์๊ฐ ๊ฐ์ผ๋ฉด ์ ์์์์ ์์ ๊ฒ์ ์ด๋ค. |
| """ |
| if not comp: |
| return "other" |
| best, bestn = None, -1 |
| for el in _ANION_ORDER: |
| n = comp.get(el, 0) |
| if n > bestn: |
| best, bestn = el, n |
| if bestn <= 0: |
| return "other" |
| return DIVISIONS.get(best, "other") |
|
|
|
|
| def _leaderboard_body(season): |
| rows = [mask(r) for r in (_board(season).get("entries") or {}).values()] |
| anchors = _anchors(season) |
| merged = rows + anchors |
| |
| |
| |
| merged.sort(key=lambda e: (e.get("tier", 1), |
| 1 if (e.get("total") is None or e.get("held_axes")) else 0, |
| -(e.get("total") if e.get("total") is not None else 0), |
| e.get("submitted_at") or 0)) |
| n = 0 |
| div_n = {} |
| trk_n = {} |
| for e in merged: |
| |
| |
| if not e.get("division"): |
| e["division"] = division_of(e.get("composition") or {}) |
| |
| |
| unscored = e.get("total") is None or bool(e.get("held_axes")) |
| if e.get("is_anchor") or e.get("tier", 1) != 1 or unscored: |
| e["rank"] = None |
| e["div_rank"] = None |
| e["track"] = None if e.get("is_anchor") else ("mp" if e.get("known_in_mp") else "novel") |
| e["track_rank"] = None |
| else: |
| n += 1 |
| e["rank"] = n |
| |
| d = e["division"] |
| div_n[d] = div_n.get(d, 0) + 1 |
| e["div_rank"] = div_n[d] |
| |
| |
| |
| |
| |
| tr = "mp" if e.get("known_in_mp") else "novel" |
| e["track"] = tr |
| trk_n[tr] = trk_n.get(tr, 0) + 1 |
| e["track_rank"] = trk_n[tr] |
| tally = {} |
| for e in merged: |
| if e.get("is_anchor"): |
| continue |
| tally[e["division"]] = tally.get(e["division"], 0) + 1 |
| n_held = sum(1 for e in merged |
| if not e.get("is_anchor") |
| and (e.get("total") is None or e.get("held_axes"))) |
| strip_masked(merged) |
| return {"season": seasons.public(season), "entries": merged, |
| "divisions": tally, |
| "counts": {"scored": len(rows) - n_held, "held": n_held, |
| "anchors": len(anchors)}} |
|
|
|
|
| def _leaderboard_bytes(season): |
| raw = json.dumps(_leaderboard_body(season), ensure_ascii=False).encode("utf-8") |
| return raw, gzip.compress(raw, 6) |
|
|
|
|
| @app.get("/api/leaderboard") |
| def leaderboard(request: Request): |
| """์๋ต์ ์์ฆ์๋ง ์์กดํ๋ฏ๋ก TTL ๋น ํ ๋ฒ๋ง ๋ง๋ค๊ณ ํ ๋ฒ๋ง ์์ถํ๋ค.""" |
| season = seasons.get(request.query_params.get("season")) |
| raw, gz = CACHE.get("lbresp:%d" % season["number"], |
| lambda: _leaderboard_bytes(season)) |
| if "gzip" in (request.headers.get("accept-encoding") or ""): |
| return Response(content=gz, media_type="application/json", |
| headers={"Content-Encoding": "gzip", "Vary": "Accept-Encoding"}) |
| return Response(content=raw, media_type="application/json", |
| headers={"Vary": "Accept-Encoding"}) |
|
|
|
|
| _FILLER = {"ai", "model", "llm", "the", "v", "ver", "version", "latest", "preview", |
| "chat", "instruct", "it", "api", "official", "new"} |
|
|
|
|
| def canon_model(name): |
| """ํ๊ธฐ๊ฐ ๊ฐ๋ฆฐ ๊ฐ์ ๋ชจ๋ธ์ ํ๋๋ก ๋ฌถ๋๋ค. |
| |
| 'Anthropic Claude Opus 5' ์ 'claude-opus-5' ๋ ๊ฐ์ ๊ฒ์ด๊ณ , ๋ฐ๋ก ์ธ๋ฉด ๋ ๋ค |
| ์ค์ ๋ณด๋ค ์ ๊ฒ ์ฐ์ธ ๊ฒ์ฒ๋ผ ๋ณด์ธ๋ค. ๋ค๋ง ๋ณ์ข
์ ๊ตฌ๋ถํ๋ ๋ฑ๋ง(Pro/Flash ๋ฑ)์ |
| ์ง์ฐ์ง ์๋๋ค - ๊ทธ๊ฒ๊น์ง ๋ญ์น๋ฉด ๋ค๋ฅธ ๋ชจ๋ธ์ด ํ ์นธ์ ๋ค์ด๊ฐ๋ค. |
| """ |
| s = re.sub(r"[^a-z0-9]+", " ", (name or "").lower()).strip() |
| toks = [t for t in s.split() if t and t not in _FILLER] |
| return " ".join(toks) or (name or "").strip().lower() |
|
|
|
|
| @app.get("/api/models") |
| def models(request: Request): |
| """๋ชจ๋ธ๋ณ ์ฑ์ : ์ด๋ ๋ชจ๋ธ์ด ๊ฐ์ฅ ์ข์ ํ๋ณด๋ฅผ ๋๋๊ฐ, ๊ทธ๋ฆฌ๊ณ ์ผ๋ง๋ ์ฐ์ด๋๊ฐ. |
| |
| ๋ ์๋ฅผ **๋ฐ๋ก** ๋ธ๋ค. ๋ง์ด ์ฐ์ธ๋ค๊ณ ์ํ๋ ๊ฒ์ด ์๋๋ค - ๋ค๋ค ์ง๋ ๋ชจ๋ธ์ ์ ์์ |
| ๋ฌด๊ดํ๊ฒ ์ ์ถ ์๊ฐ ์์ด๊ณ , ์ธ ๋ฒ ์ฐ์ธ ๋ชจ๋ธ์ด 1์ ํ๋ณด๋ฅผ ๊ฐ๊ณ ์์ ์ ์๋ค. |
| ํ๊ท ๋ง ๋ด๋ฉด ๋ ๋ค ๊ฐ๋ ค์ง๋ค. ๊ทธ๋์ ์ต๊ณ ยทํ๊ท ยท๊ฑด์๋ฅผ ๋๋ํ ๋๋ค. |
| ๊ธฐ์ค๋ฌผ์ง์ ์ ์ธํ๋ค - ์ฐธ๊ฐ์์ ๋ชจ๋ธ์ด ๋ง๋ ๊ฒ์ด ์๋๋ค. |
| """ |
| season = seasons.get(request.query_params.get("season")) |
| agg = {} |
| for r in (_board(season).get("entries") or {}).values(): |
| if r.get("total") is None: |
| continue |
| name = (r.get("model_name") or "").strip() or "๋ฏธ๊ธฐ์ฌ / unspecified" |
| key = canon_model(name) |
| a = agg.setdefault(key, {"n": 0, "best": None, "sum": 0.0, |
| "top_candidate": None, "spellings": {}}) |
| a["spellings"][name] = a["spellings"].get(name, 0) + 1 |
| a["n"] += 1 |
| a["sum"] += r["total"] |
| if a["best"] is None or r["total"] > a["best"]: |
| a["best"] = r["total"] |
| a["top_candidate"] = r.get("candidate_id") |
| out = [] |
| for k, a in agg.items(): |
| |
| label = max(a["spellings"].items(), key=lambda kv: kv[1])[0] |
| out.append({"model": label, "n": a["n"], "best": round(a["best"], 2), |
| "mean": round(a["sum"] / a["n"], 2), |
| "top_candidate": a["top_candidate"]}) |
| out.sort(key=lambda x: (-x["best"], -x["n"])) |
| return {"models": out, "counts": {"models": len(out), |
| "entries": sum(m["n"] for m in out)}} |
|
|
|
|
| @app.get("/api/queue") |
| def queue(request: Request): |
| season = seasons.get(request.query_params.get("season")) |
| ids = _submissions(season) |
| scored = set((_board(season).get("entries") or {}).keys()) |
| return {"queued": len([i for i in ids if i not in scored]), |
| "scored": len(scored), "total": len(ids)} |
|
|
|
|
| @app.get("/api/health") |
| def health(): |
| return {"ok": True, "seasons": [s["number"] for s in seasons.SEASONS]} |
|
|