SeaWolf-AI's picture
rank: separate MP and novel tracks
70561cd verified
Raw
History Blame Contribute Delete
22 kB
# -*- coding: utf-8 -*-
"""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")
# ํŽ˜์ด์ง€ ํด๋ง ์ฃผ๊ธฐ(20์ดˆ)๋ณด๋‹ค ํฌ๊ฒŒ ์žก๋Š”๋‹ค.
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 # ์ฐธ๊ฐ€์ž๊ฐ€ ๊ฐ€์ •ํ•  ํ•˜๋ฃจ ๊ฒฝ๊ณ„ (KST)
app = FastAPI(title="Open Materials Challenge")
app.add_middleware(GZipMiddleware, minimum_size=1024)
class Submission(BaseModel):
formula: str # ์กฐ์„ฑ. ์˜ˆ: Li3YCl6, LiZr2(PO4)3
display_name: str # ์ˆœ์œ„ํ‘œ์— ํ‘œ์‹œ๋  ์ด๋ฆ„
model_name: str = "" # ์–ด๋–ค ๋ชจ๋ธ์ด ์ œ์•ˆํ–ˆ๋Š”์ง€. ์ž์œ  ๊ธฐ์žฌ
rationale: str = "" # ์„ค๊ณ„ ๊ทผ๊ฑฐ (์„ ํƒ)
structure: str = "" # CIF (์„ ํƒ) - ๋‚ด๋ฉด ์ฑ„์ ์ด ๋น ๋ฅด๊ณ  ์ •ํ™•ํ•ด์ง„๋‹ค
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():
# ์ •์  ํŽ˜์ด์ง€๋Š” ์š”์ฒญ ์Šค๋ ˆ๋“œํ’€์„ ํƒ€์ง€ ์•Š๊ฒŒ async ๋กœ ๋‘”๋‹ค.
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"]
# ์—ด์‡ ๋Š” **๊ธฐ์•ฝ ์กฐ์„ฑ**์œผ๋กœ ๋งŒ๋“ ๋‹ค. Li3YCl6 ์™€ Li9Y3Cl18 ์€ ๊ฐ™์€ ๋ฌผ์งˆ์ด๋‹ค.
# ์›๋ณธ ์กฐ์„ฑ์œผ๋กœ ๋งŒ๋“ค๋ฉด ๋ฐฐ์ˆ˜๋งŒ ๋ฐ”๊ฟ” ๊ฐ™์€ ๋ฌผ์งˆ์„ ๋ช‡ ๋ฒˆ์ด๊ณ  ์˜ฌ๋ฆด ์ˆ˜ ์žˆ๋‹ค.
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:
# ์ฐธ๊ฐ€ ์ œ์ถœ์€ mask() ๊ฐ€ ์กฐ์„ฑ์—์„œ ๊ตฌ์—ญ์„ ๋ฝ‘์•„ ๋„ฃ์–ด ๋‘์—ˆ๋‹ค. ๊ธฐ์ค€๋ฌผ์งˆ์€ mask ๋ฅผ
# ๊ฑฐ์น˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ ์ฑ„์šด๋‹ค - ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋ฎ์œผ๋ฉด ์ „๋ถ€ other ๊ฐ€ ๋œ๋‹ค.
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]
# **ํŠธ๋ž™ ์ˆœ์œ„.** ๊ฐ™์€ ์กฐ์„ฑ์„ ๋‘ ๊ฒฝ๋กœ๋กœ ์ฑ„์ ํ•ด ์žฌ๋ณด๋‹ˆ ์‹ ๊ทœ ์กฐ์„ฑ ์ชฝ์ด
# ๊ณ„ํ†ต์ ์œผ๋กœ ์†ํ•ด์˜€๋‹ค(32๊ฑด ์ค‘ 28๊ฑด, ํ‰๊ท  1~2์ , ์ตœ๋Œ€ 3.5์ ). MP ์— ๊ฐ’์ด
# ์žˆ์œผ๋ฉด ๊ทธ ๊ฐ’์„ ์“ฐ๊ณ  ์—†์œผ๋ฉด ์šฐ๋ฆฌ๊ฐ€ ๊ณ„์‚ฐํ•˜๋Š”๋ฐ, ๋‘ ์ž์˜ ๋ˆˆ๊ธˆ์ด ๋‹ค๋ฅด๋‹ค.
# ๋ˆˆ๊ธˆ์ด ๋‹ค๋ฅธ ๊ฒƒ์„ ํ•œ ์ค„๋กœ ์„ธ์šฐ๋ฉด "์ƒˆ๋กœ์šด ์กฐ์„ฑ์ผ์ˆ˜๋ก ๋ถˆ๋ฆฌํ•œ" ์ˆœ์œ„ํ‘œ๊ฐ€ ๋œ๋‹ค -
# ์‹ ๊ทœ์„ฑ์— ๋ฐฐ์ ์„ ๊ฑธ์–ด ๋†“๊ณ  ๊ทธ๋Ÿฌ๋ฉด ๊ทธ ๋ฐฐ์ ์ด ๊ฑฐ์ง“๋ง์ด ๋œ๋‹ค.
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]}