leadboard / app.py
SeaWolf-AI's picture
submit: reject wrong-shaped predictions with a clear message
2b82572 verified
Raw
History Blame Contribute Delete
23.1 kB
# -*- coding: utf-8 -*-
"""LEADBOARD โ€” ์‹ ์•ฝ ์˜ˆ์ธก ๋„๊ตฌ๋ฅผ ๋ถ„์•ผ๋ณ„๋กœ ๊ฐ™์€ ์žฃ๋Œ€์— ์„ธ์šฐ๋Š” ์‹œํ—˜๋Œ€.
**์ด ์„œ๋น„์Šค๊ฐ€ ํ•˜๋Š” ์ผ**
ยท ์นดํ…Œ๊ณ ๋ฆฌยท๋ถ€๋ฌธ ์นด๋“œ๋ฅผ ์„œ๋น™ํ•œ๋‹ค
ยท ํ…Œ์ŠคํŠธ์…‹(๊ตฌ์กฐ๋งŒ)์„ ๋‚ด๋ ค์ค€๋‹ค
ยท ์ œ์ถœ์„ ๋ฐ›์•„ ๋น„๊ณต๊ฐœ ์›์žฅ์— ์ ๋Š”๋‹ค
ยท ์›Œ์ปค๊ฐ€ ๊ตด๋ ค ๋†“์€ ์ˆœ์œ„ํ‘œ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค
**ํ•˜์ง€ ์•Š๋Š” ์ผ: ์ฑ„์ .**
์ •๋‹ต์€ ์ด ์ปจํ…Œ์ด๋„ˆ์— ์—†๋‹ค. ์ปจํ…Œ์ด๋„ˆ ์ด๋ฏธ์ง€๋Š” ๋ˆ„๊ตฌ๋‚˜ ๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์—ฌ๊ธฐ์— ๋ผ๋ฒจ์„ ๋‘๋ฉด
๊ทธ ์ˆœ๊ฐ„ 1์กฐ๊ฐ€ ๋ฌด๋„ˆ์ง„๋‹ค. ์ฑ„์ ์€ ์›์žฅ์„ ํด๋งํ•˜๋Š” ๋ณ„๋„ ์›Œ์ปค๊ฐ€ ๋กœ์ปฌ ์ •๋‹ต ํŒŒ์ผ๋กœ ํ•œ๋‹ค.
๋ถ€๋ฌธ ์นด๋“œ์—๋Š” **์ฑ„์ ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ณต๊ฐœ ์ •๋ณด**๊ฐ€ ๋“ค์–ด ์žˆ๋‹ค โ€”
๋ถ„ํ•  ๋“ฑ๊ธ‰ ยท ์ •๋‹ต ๋“ฑ๊ธ‰ ยท ์žก์Œ ๋ฐ”๋‹ฅ ยท ๊ธฐ์ค€์„  ์„ฑ์  ยท ๋ฐ์ดํ„ฐ ์ง€๋ฌธ.
์šฐ๋ฆฌ ์ ์ˆ˜๋ฅผ ๋ฏฟ์–ด๋‹ฌ๋ผ๊ณ  ํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด์„œ๋‹ค.
"""
import base64
import glob
import hashlib
import hmac
import io
import json
import os
import re
import secrets
import time
import urllib.error
import urllib.parse
import urllib.request
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import (FileResponse, JSONResponse, RedirectResponse,
Response)
from pydantic import BaseModel
HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data")
SPEC = "v1.1"
LEDGER = os.environ.get("LB_LEDGER_REPO", "FINAL-Bench/leadboard-submissions")
LEDGER_API = "https://huggingface.co/api/datasets/%s" % LEDGER
LEDGER_RAW = "https://huggingface.co/datasets/%s/resolve/main" % LEDGER
HF_TOKEN = os.environ.get("HF_TOKEN", "")
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 = "lb_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("LB_SESSION_KEY") or secrets.token_hex(16)
DAILY_CAP = int(os.environ.get("LB_DAILY_CAP", "5"))
app = FastAPI(title="LEADBOARD")
app.add_middleware(GZipMiddleware, minimum_size=1024)
_C = {}
# ํ•˜๋ฃจ ํ•œ๋„ ์นด์šดํ„ฐ. ์ปจํ…Œ์ด๋„ˆ ์žฌ๊ธฐ๋™์ด๋ฉด ๋น„๋Š”๋ฐ, ๊ทธ๋ž˜๋„ ํญ์ฃผ๋Š” ๋ง‰๋Š”๋‹ค.
# ์ •ํ™•ํ•œ ํšŒ๊ณ„๊ฐ€ ํ•„์š”ํ•ด์ง€๋ฉด ์›์žฅ ์ชฝ์œผ๋กœ ์˜ฎ๊ธด๋‹ค.
_CAP = {}
def cached(key, ttl, produce):
hit = _C.get(key)
if hit and time.time() - hit[0] < ttl:
return hit[1]
try:
v = produce()
except Exception:
if hit:
return hit[1]
raise
_C[key] = (time.time(), v)
return v
# ------------------------------------------------------------------ ๋ถ€๋ฌธ ์นด๋“œ
def _sha(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for b in iter(lambda: f.read(65536), b""):
h.update(b)
return h.hexdigest()[:16]
def _board_meta():
"""๋ถ€๋ฌธ ํ•œ ์ค„ ์„ค๋ช…. ์ด๋ฆ„๊ณผ ํ‘œ์  ๊ธฐํ˜ธ๋งŒ์œผ๋กœ๋Š” ๋ฌด์—‡์„ ์žฌ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๋‹ค."""
p = os.path.join(HERE, "board_meta.json")
try:
return json.load(io.open(p, encoding="utf-8")).get("boards", {})
except Exception:
return {}
def _load_boards():
meta = _board_meta()
out = {}
for p in sorted(glob.glob(os.path.join(DATA, "*_card.json"))):
try:
d = json.load(io.open(p, encoding="utf-8"))
except Exception:
continue
nf = d.get("noise_floor") or {}
base = d.get("baselines") or {}
# ๋ถ„๋ฅ˜ ์ „์šฉ ๋ถ€๋ฌธ์—๋Š” MAE ๊ธฐ์ค€์„ ์ด ์—†๋‹ค. ๊ทธ๋Ÿด ๋•Œ๋Š” AUROC ๊ฐ€ ๊ฐ€์žฅ ๋†’์€ ๊ฒƒ์„ ์ตœ์„ ์œผ๋กœ ๋ณธ๋‹ค.
# ์—†๋Š” ๊ฐ’์„ 0 ์œผ๋กœ ์น˜๋ฉด ๊ทธ ๋ถ€๋ฌธ์ด "๊ฐ€์žฅ ์ •ํ™•ํ•œ ๊ณณ"์œผ๋กœ ํ‘œ์— ์˜ค๋ฅธ๋‹ค.
with_mae = {k: v for k, v in base.items() if v.get("mae") is not None}
if with_mae:
best = min(with_mae, key=lambda k: with_mae[k]["mae"])
d["metric"] = "mae"
elif base:
best = max(base, key=lambda k: base[k].get("auroc") or 0)
d["metric"] = "auroc"
else:
best = None
d["metric"] = None
d["dataset_sha"] = _sha(p)
d["spec"] = SPEC
d["best_baseline"] = best
# ๋ชจ๋ธ ์˜ค์ฐจ๊ฐ€ ์‹คํ—˜ ์˜ค์ฐจ์˜ ๋ช‡ ๋ฐฐ์ธ๊ฐ€. 1 ์— ๋ถ™์„์ˆ˜๋ก ์ธก์ • ํ•œ๊ณ„๋‹ค.
if best and nf.get("sd_single") and d["metric"] == "mae":
d["error_over_noise"] = round(base[best]["mae"] / nf["sd_single"], 2)
if ("์ƒ์ˆ˜ ์˜ˆ์ธก" in base and "Morgan+LightGBM" in base
and base["Morgan+LightGBM"].get("mae") is not None
and base["์ƒ์ˆ˜ ์˜ˆ์ธก"].get("mae") is not None):
d["beats_constant"] = base["Morgan+LightGBM"]["mae"] < base["์ƒ์ˆ˜ ์˜ˆ์ธก"]["mae"]
if d.get("n_test"):
d["near_pct"] = round(100.0 * d.get("near_threshold", 0) / d["n_test"], 1)
m = meta.get(d["board"]) or {}
d["blurb"] = m.get("ko", "")
d["blurb_en"] = m.get("en", "")
d["open"] = True
out[d["board"]] = d
return out
def boards():
return cached("boards", 300, _load_boards)
def _load_references():
"""ํ˜„์—… ๋„๊ตฌ๋ฅผ ์ด ๋ถ€๋ฌธ ํ›ˆ๋ จ ์ž๋ฃŒ๋กœ ์žฌํ•™์Šตํ•ด ์–ป์€ ์„ฑ์ .
๋‹ค๋ฅธ ์ž๋ฃŒ๋กœ ํ•™์Šตํ•œ ๋ชจ๋ธ์„ ๊ทธ๋Œ€๋กœ ์˜ฎ๊ฒจ ์žฌ๋ฉด, ํ›ˆ๋ จ ์ž๋ฃŒ์˜ ์ฐจ์ด๊ฐ€ ๋ฐฉ๋ฒ•์˜ ์ฐจ์ด๋กœ ์ฝํžŒ๋‹ค.
๊ทธ๋ž˜์„œ **๋ฐฉ๋ฒ•๋งŒ ๊ฐ€์ ธ์˜ค๊ณ  ๋ฐ์ดํ„ฐ๋Š” ์ด ๋ถ€๋ฌธ ๊ฒƒ์„ ์“ด๋‹ค.** ๊ทธ๋ž˜์•ผ ํ‘œ์— ์˜ค๋ฅธ ์ˆซ์ž๊ฐ€
"์ด ๋ฐฉ๋ฒ•์ด ์ด ๋ฌธ์ œ์—์„œ ์–ด๋””๊นŒ์ง€ ๊ฐ€๋Š”๊ฐ€"๋ฅผ ๋œปํ•œ๋‹ค.
"""
# ์ฐธ์กฐ ๋ฐฉ๋ฒ•์€ ์—ฌ๋Ÿฟ์ผ ์ˆ˜ ์žˆ๋‹ค. ํŒŒ์ผ ํ•˜๋‚˜์— ๋ฐฉ๋ฒ• ํ•˜๋‚˜๋ฅผ ๋‹ด๊ณ , ์—ฌ๊ธฐ์„œ ๋‹ค ๋ชจ์€๋‹ค.
out = {}
for p in sorted(glob.glob(os.path.join(DATA, "reference_*.json"))):
try:
d = json.load(io.open(p, encoding="utf-8"))
except Exception:
continue
for e in d.get("entries", []):
out.setdefault(e["board"], []).append(
dict(e, method=d.get("method", "reference"),
method_en=d.get("method_en") or d.get("method", "reference"),
ref_note=d.get("note"), ref_note_en=d.get("note_en")))
return out
def references():
return cached("refs", 300, _load_references)
def categories():
def build():
cats = json.load(io.open(os.path.join(HERE, "categories.json"), encoding="utf-8"))
bd = boards()
for c in cats["categories"]:
c["open"] = len([b for b in c["boards"] if b in bd])
c["board_cards"] = [bd[b] for b in c["boards"] if b in bd]
return cats
return cached("cats", 300, build)
# ------------------------------------------------------------------ ์›์žฅ
def _hdr():
return {"Authorization": "Bearer " + HF_TOKEN, "User-Agent": "LEADBOARD/1.0"}
def ledger_read(path, default=None):
try:
with urllib.request.urlopen(urllib.request.Request(
"%s/%s" % (LEDGER_RAW, path), headers=_hdr()), timeout=60) as r:
return json.loads(r.read())
except Exception:
return default
def ledger_write(path, obj, summary):
blob = base64.b64encode(json.dumps(obj, ensure_ascii=False).encode()).decode()
lines = [json.dumps({"key": "header", "value": {"summary": summary}}),
json.dumps({"key": "file", "value": {"path": path, "content": blob,
"encoding": "base64"}})]
req = urllib.request.Request(LEDGER_API + "/commit/main",
data=("\n".join(lines) + "\n").encode(),
headers=dict(_hdr(), **{"Content-Type": "application/x-ndjson"}))
with urllib.request.urlopen(req, timeout=180) as r:
return json.loads(r.read())
# ------------------------------------------------------------------ ์„ธ์…˜
def sign(v):
return hmac.new(SESSION_KEY.encode(), v.encode(), hashlib.sha256).hexdigest()[:32]
def set_session(resp, user):
raw = json.dumps(user, ensure_ascii=False)
b = base64.urlsafe_b64encode(raw.encode()).decode()
resp.set_cookie(COOKIE, "%s.%s" % (b, sign(b)), max_age=86400 * 7,
httponly=True, **COOKIE_KW)
def who(req: Request):
c = req.cookies.get(COOKIE) or ""
if "." not in c:
return None
b, sg = c.rsplit(".", 1)
if not hmac.compare_digest(sg, sign(b)):
return None
try:
return json.loads(base64.urlsafe_b64decode(b.encode()).decode())
except Exception:
return None
# ------------------------------------------------------------------ ๋ผ์šฐํŠธ
@app.get("/")
def index():
return FileResponse(os.path.join(HERE, "index.html"))
@app.get("/i18n.js")
def i18n():
"""๋ฌธ์ž์—ด ์‚ฌ์ „. ํ™”๋ฉด ์ฝ”๋“œ์™€ ๋ถ„๋ฆฌํ•ด ๋‘์–ด์•ผ ์˜์–ดํŒ์ด ์กฐ์šฉํžˆ ๋’ค์ฒ˜์ง€์ง€ ์•Š๋Š”๋‹ค."""
return FileResponse(os.path.join(HERE, "i18n.js"),
media_type="application/javascript")
@app.get("/api/categories")
def api_categories():
c = categories()
bd = boards()
return {"spec": SPEC, "categories": c["categories"],
"totals": {"planned": sum(x["planned"] for x in c["categories"]),
"open": len(bd), "ledger": bool(HF_TOKEN)}}
@app.get("/api/leaders")
def api_leaders():
"""๋ถ€๋ฌธ๋งˆ๋‹ค ํ˜„์žฌ 1์œ„ ํ•œ ์ค„. ์ฒซ ํ™”๋ฉด์—์„œ ์ „์ฒด๋ฅผ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ ์œ„ํ•œ ๊ฒƒ์ด๋‹ค.
๋ถ€๋ฌธ๋ณ„๋กœ ์ˆœ์œ„ํ‘œ๋ฅผ ๋”ฐ๋กœ ๋ถ€๋ฅด๋ฉด ์™•๋ณต์ด ๋ถ€๋ฌธ ์ˆ˜๋งŒํผ ๋Š˜์–ด๋‚œ๋‹ค. ์—ฌ๊ธฐ์„œ ํ•œ ๋ฒˆ์— ๋ชจ์€๋‹ค.
**์ฐธ๊ฐ€ ์ œ์ถœ์ด ์—†์œผ๋ฉด ๋น„์›Œ์„œ ๋ณด๋‚ธ๋‹ค** - ๊ธฐ์ค€์„ ์„ 1์œ„ ์ž๋ฆฌ์— ์•‰ํžˆ์ง€ ์•Š๋Š”๋‹ค.
๊ธฐ์ค€์„ ์€ ๋„˜์–ด์•ผ ํ•  ์„ ์ด์ง€ ์šฐ์Šน์ž๊ฐ€ ์•„๋‹ˆ๋‹ค.
"""
def build():
out = []
for name, b in boards().items():
rolled = ledger_read("leaderboard/%s.json" % name.lower(), {}) or {}
ent = (rolled.get("entries") or [])
top = ent[0] if ent else None
nf = (b.get("noise_floor") or {}).get("sd_single")
base = b.get("baselines") or {}
row = {"board": name, "n_test": b.get("n_test"),
"blurb": b.get("blurb"), "blurb_en": b.get("blurb_en"),
"split_grade": b.get("split_grade"), "answer_grade": b.get("answer_grade"),
"noise_floor": nf, "entries": len(ent),
"baseline_best": (min((v["mae"] for v in base.values() if v.get("mae") is not None),
default=None)),
"baseline_best_auroc": (max((v["auroc"] for v in base.values()
if v.get("auroc") is not None), default=None)),
"leader": None}
if top:
row["leader"] = {"method": top.get("method"), "user": top.get("user"),
"mae": top.get("mae"), "auroc": top.get("auroc"),
"verified": bool(top.get("verified")), "leak": top.get("leak")}
out.append(row)
out.sort(key=lambda r: (r["leader"] is None, -(r["n_test"] or 0)))
return out
rows = cached("leaders", 60, build)
return {"spec": SPEC, "n": len(rows), "rows": rows,
"held": sum(1 for r in rows if r["leader"])}
@app.get("/api/board/{name}")
def api_board(name: str):
b = boards().get(name)
if not b:
raise HTTPException(404, "๊ทธ๋Ÿฐ ๋ถ€๋ฌธ์ด ์—†๋‹ค")
return b
@app.get("/api/board/{name}/testset")
def api_testset(name: str):
"""ํ…Œ์ŠคํŠธ์…‹. ๊ตฌ์กฐ๋งŒ ๋‚˜๊ฐ„๋‹ค - ๋ผ๋ฒจ์€ ์ด ์ปจํ…Œ์ด๋„ˆ์— ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค."""
p = os.path.join(DATA, "%s_test.json" % name.lower())
if not os.path.exists(p):
raise HTTPException(404, "ํ…Œ์ŠคํŠธ์…‹์ด ์•„์ง ์—†๋‹ค")
return FileResponse(p, media_type="application/json",
filename="%s_test.json" % name.lower())
@app.get("/api/board/{name}/leaderboard")
def api_leaderboard(name: str):
"""์ˆœ์œ„ํ‘œ. ๊ธฐ์ค€์„ ์€ ํ•ญ์ƒ ํฌํ•จ๋œ๋‹ค (3์กฐ).
์ฐธ๊ฐ€์ž ํ•ญ๋ชฉ์€ ์›Œ์ปค๊ฐ€ ๊ตด๋ ค ๋†“์€ ๊ฒƒ์„ ๊ทธ๋Œ€๋กœ ๋ณด์—ฌ์ค€๋‹ค. ์—ฌ๊ธฐ์„œ ๊ณ„์‚ฐํ•˜์ง€ ์•Š๋Š”๋‹ค -
๊ณ„์‚ฐํ•˜๋ ค๋ฉด ์ •๋‹ต์ด ์žˆ์–ด์•ผ ํ•˜๊ณ , ์ •๋‹ต์€ ์—ฌ๊ธฐ ์—†๋‹ค.
"""
b = boards().get(name)
if not b:
raise HTTPException(404, "๊ทธ๋Ÿฐ ๋ถ€๋ฌธ์ด ์—†๋‹ค")
rolled = cached("lb:" + name, 60,
lambda: ledger_read("leaderboard/%s.json" % name.lower(),
{"entries": [], "updated": None})) or {}
rows = []
for k, v in (b.get("baselines") or {}).items():
rows.append({"method": k, "user": "โ€”", "kind": "baseline",
"mae": v["mae"], "auroc": v["auroc"], "prauc": v["prauc"]})
# ํ˜„์—… ๋„๊ตฌ ์ฐธ์กฐ ํ•ญ๋ชฉ. ๊ธฐ์ค€์„ ๊ณผ ์ฐธ๊ฐ€ ์ œ์ถœ ์‚ฌ์ด์— ๋†“๋Š”๋‹ค - ํ•™์Šตํ•˜์ง€ ์•Š์€ ์„ ๋„ ์•„๋‹ˆ๊ณ 
# ์ด๋ฒˆ ํšŒ์ฐจ์˜ ์ฐธ๊ฐ€์ž๋„ ์•„๋‹ˆ๋‹ค. ์ฐธ๊ฐ€์ž๊ฐ€ ์ž๊ธฐ ์œ„์น˜๋ฅผ ๊ฐ€๋Š ํ•  ์„ธ ๋ฒˆ์งธ ์ขŒํ‘œ๋‹ค.
for r in references().get(name, []):
rows.append({"method": r["method"], "method_en": r.get("method_en"),
"user": "โ€”", "kind": "reference",
"mae": r.get("mae"), "auroc": r.get("auroc"),
"prauc": r.get("prauc"), "leak": r.get("leak"),
"note": r.get("ref_note"), "note_en": r.get("ref_note_en")})
for e in rolled.get("entries", []):
rows.append(dict(e, kind=e.get("kind", "entry")))
# ๋ถ€๋ฌธ ์ฃผ์ง€ํ‘œ๋กœ ์„ธ์šด๋‹ค. ๋ถ„๋ฅ˜ ์ „์šฉ ๋ถ€๋ฌธ์—์„œ mae ๋กœ ์„ธ์šฐ๋ฉด ์ „๋ถ€ ๋™๋ฅ ์ด ๋œ๋‹ค.
if b.get("metric") == "auroc":
rows.sort(key=lambda r: -(r.get("auroc") or 0))
else:
rows.sort(key=lambda r: (r.get("mae") is None, r.get("mae") or 9e9))
nf = (b.get("noise_floor") or {}).get("sd_single")
best = rows[0].get("mae") if rows else None
# 4์กฐ: ์ตœ๊ณ ์ ์—์„œ ์žก์Œ ๋ฐ”๋‹ฅ ์•ˆ์— ๋“  ํ•ญ๋ชฉ์€ ๊ฐ™์€ ๊ณ„๋‹จ์œผ๋กœ ๋ฌถ๋Š”๋‹ค.
for r in rows:
r["within_noise"] = bool(nf and best is not None and r.get("mae") is not None
and r["mae"] - best < nf)
rank = 0
for r in rows:
# ์ˆœ์œ„๋Š” ์ฐธ๊ฐ€ ์ œ์ถœ์—๋งŒ ๋งค๊ธด๋‹ค. ๊ธฐ์ค€์„ ๊ณผ ์ฐธ์กฐ ๋„๊ตฌ๋Š” ํ‘œ์— ์„œ๋˜ ๋“ฑ์ˆ˜๋ฅผ ๊ฐ–์ง€ ์•Š๋Š”๋‹ค -
# ์šฐ๋ฆฌ๊ฐ€ ์˜ฌ๋ฆฐ ๊ฒƒ์ด 1์œ„ ์ž๋ฆฌ๋ฅผ ์ฐจ์ง€ํ•˜๋ฉด ์ฐธ๊ฐ€์ž์—๊ฒŒ ๊ฒจ๋ฃฐ ์ž๋ฆฌ๊ฐ€ ์—†๋‹ค.
if r["kind"] in ("baseline", "reference"):
r["rank"] = None
else:
rank += 1
r["rank"] = rank
return {"board": name, "noise_floor": nf, "rows": rows,
"updated": rolled.get("updated"),
"note": "์žก์Œ ๋ฐ”๋‹ฅ ์•ˆ์— ๋“  ํ•ญ๋ชฉ์€ ์ˆœ์œ„ ์ฐจ์ด๋กœ ์ฃผ์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค (4์กฐ)"}
# ------------------------------------------------------------------ ๋กœ๊ทธ์ธ
@app.get("/login")
def login(request: Request):
if not (OAUTH_ID and OAUTH_SECRET):
return _err_page("์ด ์‹œํ—˜๋Œ€์— ๋กœ๊ทธ์ธ์ด ์•„์ง ๊ตฌ์„ฑ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
nxt = request.query_params.get("next", "/")
st = base64.urlsafe_b64encode(json.dumps({"n": nxt, "r": secrets.token_hex(8)}).encode()).decode()
q = urllib.parse.urlencode({
"client_id": OAUTH_ID, "redirect_uri": _redirect(request),
"response_type": "code", "scope": "openid profile", "state": st})
return RedirectResponse("%s/oauth/authorize?%s" % (OAUTH_ISS, q))
def _redirect(request: Request):
"""ํ”Œ๋žซํผ์ด ๋“ฑ๋กํ•ด ์ฃผ๋Š” ์ฝœ๋ฐฑ ์ฃผ์†Œ๋Š” **/auth/callback** ์ด๋‹ค.
์—ฌ๊ธฐ๋ฅผ /auth ๋กœ ๋‘๋ฉด ํ† ํฐ ๊ตํ™˜์—์„œ redirect_uri ๋ถˆ์ผ์น˜๋กœ ๊ฑฐ๋ถ€๋˜๊ณ , ๊ทธ ์˜ˆ์™ธ๊ฐ€
๊ทธ๋Œ€๋กœ 500 ์ด ๋˜์–ด ํ™”๋ฉด ์ „์ฒด๊ฐ€ ์ฃฝ๋Š”๋‹ค. ๋กœ๊ทธ์ธ ํ•œ ๋ฒˆ ๋ˆŒ๋ €๋‹ค๊ฐ€ ์‚ฌ์ดํŠธ๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค.
"""
if SPACE_HOST:
return "https://%s/auth/callback" % SPACE_HOST
return str(request.base_url).rstrip("/") + "/auth/callback"
def _err_page(msg, detail=""):
"""๋กœ๊ทธ์ธ์ด ์‹คํŒจํ•ด๋„ ํ™”๋ฉด์€ ์‚ด์•„ ์žˆ์–ด์•ผ ํ•œ๋‹ค. ํฐ ๋ฐฐ๊ฒฝ์— Internal Server Error ๋งŒ
๋‚จ์œผ๋ฉด ์ด์šฉ์ž๋Š” ์‚ฌ์ดํŠธ๊ฐ€ ์ฃฝ์€ ์ค„ ์•ˆ๋‹ค."""
return Response(
"<!doctype html><html lang=ko><head><meta charset=utf-8>"
"<title>๋กœ๊ทธ์ธ ์‹คํŒจ</title><style>body{font-family:system-ui,sans-serif;"
"max-width:38em;margin:14vh auto;padding:0 24px;line-height:1.7;color:#15171c}"
"code{background:#f0f3f7;padding:2px 6px;border-radius:5px;font-size:13px}"
"a{color:#1f4fa3}</style></head><body>"
"<h2>๋กœ๊ทธ์ธ์„ ๋งˆ์น˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค</h2><p>%s</p>%s"
"<p><a href='/'>โ† ์‹œํ—˜๋Œ€๋กœ ๋Œ์•„๊ฐ€๊ธฐ</a></p></body></html>"
% (msg, ("<p><code>%s</code></p>" % detail[:300]) if detail else ""),
media_type="text/html; charset=utf-8", status_code=200)
@app.get("/auth/callback")
def auth(request: Request):
code = request.query_params.get("code")
st = request.query_params.get("state") or ""
nxt = "/"
try:
nxt = json.loads(base64.urlsafe_b64decode(st.encode()).decode()).get("n", "/")
except Exception:
pass
if not code:
return RedirectResponse(nxt)
try:
body = urllib.parse.urlencode({
"client_id": OAUTH_ID, "client_secret": OAUTH_SECRET,
"grant_type": "authorization_code", "code": code,
"redirect_uri": _redirect(request)}).encode()
with urllib.request.urlopen(urllib.request.Request(
OAUTH_ISS + "/oauth/token", data=body,
headers={"Content-Type": "application/x-www-form-urlencoded"}),
timeout=60) as r:
tok = json.loads(r.read())
with urllib.request.urlopen(urllib.request.Request(
OAUTH_ISS + "/oauth/userinfo",
headers={"Authorization": "Bearer " + tok["access_token"]}),
timeout=60) as r:
ui = json.loads(r.read())
except urllib.error.HTTPError as e:
return _err_page("์ธ์ฆ ์ œ๊ณต์ž๊ฐ€ ์š”์ฒญ์„ ๊ฑฐ๋ถ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์‹ญ์‹œ์˜ค.",
"%d %s" % (e.code, e.read().decode("utf-8", "replace")))
except Exception as e:
return _err_page("์ธ์ฆ ์ค‘ ํ†ต์‹ ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์‹ญ์‹œ์˜ค.",
"%s: %s" % (type(e).__name__, e))
resp = RedirectResponse(nxt)
set_session(resp, {"user": ui.get("preferred_username") or ui.get("sub"),
"name": ui.get("name", ""), "pic": ui.get("picture", "")})
return resp
@app.get("/auth")
def auth_legacy(request: Request):
"""์˜ˆ์ „ ์ฃผ์†Œ๋กœ ๋“ค์–ด์˜จ ์ฝœ๋ฐฑ๋„ ๋ฐ›์•„ ์ค€๋‹ค. ๋งํฌ๊ฐ€ ์–ด๋”˜๊ฐ€ ๋‚จ์•„ ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค."""
return auth(request)
@app.get("/logout")
def logout():
r = RedirectResponse("/")
r.delete_cookie(COOKIE, **COOKIE_KW)
return r
@app.get("/api/me")
def api_me(request: Request):
u = who(request)
return {"user": u, "login_enabled": bool(OAUTH_ID), "daily_cap": DAILY_CAP}
# ------------------------------------------------------------------ ์ œ์ถœ
class Submission(BaseModel):
board: str
method: str # ๋ฐฉ๋ฒ• ์ด๋ฆ„. ์ˆœ์œ„ํ‘œ์— ์ด๋ ‡๊ฒŒ ํ‘œ์‹œ๋œ๋‹ค
predictions: dict # {compound_id: ์˜ˆ์ธก๊ฐ’}
training_data: str = "" # 5์กฐ ๋ˆ„์ถœ๊ฒ€์‚ฌ์— ์“ด๋‹ค
pretrained_on: str = ""
code_url: str = ""
container: str = "" # ์žˆ์œผ๋ฉด [๊ฒ€์ฆ๋จ] ์‹ฌ์‚ฌ ๋Œ€์ƒ (6์กฐ)
@app.post("/api/submit")
def submit(s: Submission, request: Request):
u = who(request)
if not u:
raise HTTPException(401, "์ œ์ถœํ•˜๋ ค๋ฉด ๋กœ๊ทธ์ธํ•ด์•ผ ํ•œ๋‹ค")
if not HF_TOKEN:
raise HTTPException(503, "์›์žฅ์ด ๊ตฌ์„ฑ๋˜์ง€ ์•Š์•˜๋‹ค")
b = boards().get(s.board)
if not b:
raise HTTPException(404, "๊ทธ๋Ÿฐ ๋ถ€๋ฌธ์ด ์—†๋‹ค")
if not re.fullmatch(r"[\w .\-+/()]{2,60}", s.method or ""):
raise HTTPException(400, "๋ฐฉ๋ฒ• ์ด๋ฆ„์€ 2~60์ž์—ฌ์•ผ ํ•œ๋‹ค")
# ํ•˜๋ฃจ ํ•œ๋„. 8์กฐ๊ฐ€ ์ ์ˆ˜ ๊ณต๊ฐœ๋ฅผ ๋ง‰์•„๋„ ์ œ์ถœ ์ž์ฒด๋Š” ๋น„์šฉ์ด ๋“ค๊ณ , ๋ฌด์ œํ•œ์ด๋ฉด
# ์›์žฅ์ด ์ž ๊ธด๋‹ค. ๊ฒฝ๊ณ„๋Š” ์ฐธ๊ฐ€์ž๊ฐ€ ๊ฐ€์ •ํ•  ํ•˜๋ฃจ(KST)๋กœ ์žก๋Š”๋‹ค.
day = int((time.time() + 9 * 3600) // 86400)
key = "cap:%s:%d" % (u["user"], day)
used = _CAP.get(key, 0)
if used >= DAILY_CAP:
raise HTTPException(429, "์˜ค๋Š˜ ์ œ์ถœ ํ•œ๋„ %dํšŒ๋ฅผ ๋‹ค ์ผ๋‹ค. ํ•œ๊ตญ์‹œ๊ฐ„ ์ž์ •์— ์ดˆ๊ธฐํ™”๋œ๋‹ค"
% DAILY_CAP)
n = b.get("n_test", 0)
got = len(s.predictions or {})
if got < n:
raise HTTPException(400, "์˜ˆ์ธก์ด %d๊ฐœ ํ•„์š”ํ•œ๋ฐ %d๊ฐœ๋‹ค. ๋นˆ ํ•ญ๋ชฉ์€ ์ฑ„์›Œ ๋ณด๋‚ด๋ผ "
"- ์ž„์˜๋กœ ๋ฉ”์šฐ๋ฉด ๊ทธ ๊ฐ’์ด ์ ์ˆ˜์— ๋“ค์–ด๊ฐ„๋‹ค" % (n, got))
# **๋ชจ์–‘๋„ ๋ณธ๋‹ค.** ์ถ•์ด ์—ฌ๋Ÿฟ์ธ ๋ถ€๋ฌธ์— ์ˆซ์ž ํ•˜๋‚˜๋ฅผ ๋ณด๋‚ด๋ฉด ์ฑ„์ ๊ธฐ๋Š” ์ „๋ถ€ missing ์œผ๋กœ
# ์ฒ˜๋ฆฌํ•˜๊ณ  ์ ์ˆ˜๊ฐ€ ๋น„์–ด์„œ ๋Œ์•„์˜จ๋‹ค - ์ฐธ๊ฐ€์ž๋Š” ๋ฌด์—‡์ด ํ‹€๋ ธ๋Š”์ง€ ์•Œ ๊ธธ์ด ์—†๋‹ค.
# ๊ฐœ์ˆ˜๋งŒ ์„ธ๊ณ  ํ†ต๊ณผ์‹œํ‚ค๋ฉด ๊ทธ ์นจ๋ฌต์ด ์šฐ๋ฆฌ ๋ชซ์ด ๋œ๋‹ค.
ax = b.get("axes")
if ax:
bad = [k for k, v in (s.predictions or {}).items()
if not (isinstance(v, (list, tuple)) and len(v) == ax)]
if bad:
raise HTTPException(400, "์ด ๋ถ€๋ฌธ์€ ํ™”ํ•ฉ๋ฌผ๋งˆ๋‹ค ์ˆซ์ž %d๊ฐœ์งœ๋ฆฌ ๋ฐฐ์—ด์ด ํ•„์š”ํ•˜๋‹ค. "
"%d๊ฑด์ด ๊ทธ ๋ชจ์–‘์ด ์•„๋‹ˆ๋‹ค (์˜ˆ: %s). ํ˜•์‹์€ ๋ถ€๋ฌธ ์•ˆ๋‚ด๋ฅผ ๋ณด๋ผ"
% (ax, len(bad), bad[0]))
else:
bad = [k for k, v in (s.predictions or {}).items()
if isinstance(v, (list, tuple))]
if bad:
raise HTTPException(400, "์ด ๋ถ€๋ฌธ์€ ํ™”ํ•ฉ๋ฌผ๋งˆ๋‹ค ์ˆซ์ž ํ•˜๋‚˜๊ฐ€ ํ•„์š”ํ•œ๋ฐ ๋ฐฐ์—ด์ด ์™”๋‹ค "
"(%d๊ฑด, ์˜ˆ: %s)" % (len(bad), bad[0]))
sid = hashlib.sha256(("%s|%s|%s|%f" % (s.board, u["user"], s.method, time.time()))
.encode()).hexdigest()[:16].upper()
rec = {"submission_id": sid, "board": s.board, "hf_user": u["user"],
"method": s.method, "predictions": s.predictions,
"training_data": s.training_data, "pretrained_on": s.pretrained_on,
"code_url": s.code_url, "container": s.container,
"submitted_at": int(time.time()), "spec": SPEC,
"dataset_sha": b.get("dataset_sha")}
ledger_write("submissions/%s/%s.json" % (s.board.lower(), sid), rec,
"submit %s %s" % (s.board, sid))
_CAP[key] = used + 1
return {"ok": True, "submission_id": sid,
"remaining_today": DAILY_CAP - _CAP[key],
"note": "์ฑ„์ ์€ ์›Œ์ปค๊ฐ€ ํ•œ๋‹ค. 8์กฐ์— ๋”ฐ๋ผ ์ง์ „ ์ตœ๊ณ ์ ์„ ์žก์Œ ๋ฐ”๋‹ฅ ์ด์ƒ์œผ๋กœ "
"๋„˜์—ˆ์„ ๋•Œ๋งŒ ์ƒˆ ์ ์ˆ˜๊ฐ€ ๊ณต๊ฐœ๋œ๋‹ค."}
@app.get("/api/health")
def health():
return {"ok": True, "spec": SPEC, "boards": len(boards()),
"ledger": bool(HF_TOKEN), "login": bool(OAUTH_ID),
"scoring_here": False}