leadboard / app.py
SeaWolf-AI's picture
fix: ์ถ• ๋ถ€๋ฌธ(Phenotype) ์ œ์ถœ ๋ถˆ๊ฐ€ - _load_boards ๊ฐ€ axes ๋ฅผ ์•ˆ ์‹ค์—ˆ๋‹ค
50e96ee verified
Raw History Blame Contribute Delete
32 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"
# ์ƒ๊ธˆ. 46๋ถ€๋ฌธ ์ค‘ 29๋ถ€๋ฌธ์ด ๋น„์–ด ์žˆ๊ณ  ์™ธ๋ถ€ ์ฐธ๊ฐ€์ž๊ฐ€ 1๋ช…๋ฟ์ด๋ผ ๋ถ™์ธ๋‹ค. ์ˆœ์œ„ํ‘œ๋งŒ ์žˆ๊ณ 
# ์™œ ์ง€๊ธˆ ํ•ด์•ผ ํ•˜๋Š”์ง€๊ฐ€ ์—†์—ˆ๋‹ค.
#
# ๐Ÿ”ด ์ฃผ์ตœ์ธก ๊ณ„์ •์€ ์ˆ˜์ƒ ๋Œ€์ƒ์—์„œ ๋บ€๋‹ค. ํ˜„์žฌ 1์œ„ 17๊ฐœ ์ค‘ 16๊ฐœ๊ฐ€ ์ฃผ์ตœ์ธก์ด๋ผ ๊ทธ๋Œ€๋กœ ๋‘๋ฉด
# ์ฃผ์ตœ์ธก์ด ์ž๊ธฐ ์ƒ๊ธˆ์„ ๋ฐ›๋Š”๋‹ค. ODC ์—์„œ ๊ธฐ์ค€๋ฌผ์งˆ์„ ํ‘œ์— ์˜ฌ๋ฆฌ๋˜ ๋“ฑ์ˆ˜๋ฅผ ์ฃผ์ง€ ์•Š๋Š” ๊ฒƒ๊ณผ
# ๊ฐ™์€ ์ฒ˜๋ฆฌ๋‹ค - ๋ˆˆ๊ธˆ ์—ญํ• ์€ ํ•˜๋˜ ๊ฒจ๋ฃจ์ง€๋Š” ์•Š๋Š”๋‹ค.
HOST_ACCOUNTS = {"SeaWolf-AI", "FINAL-Bench", "VIDraft"}
PRIZE = {
"amount_usd": 1000,
"winners": 1,
"closes": "2027-12-31",
# **1์œ„ ๋ถ€๋ฌธ์„ ๊ฐ€์žฅ ๋งŽ์ด ๊ฐ€์ง„ ํ•œ ์‚ฌ๋žŒ.** ์ „ ๋ถ€๋ฌธ ์ œ์ถœ ์กฐ๊ฑด์€ ๋‘์ง€ ์•Š๋Š”๋‹ค
# (ํšŒ์žฅ๋‹˜ 2026-08-29 ๊ฐœ์ •) - ๋ฌธํ„ฑ์ด ์ฐธ๊ฐ€๋ฅผ ๋ง‰์œผ๋ฉด ๊ฒจ๋ฃฐ ์‚ฌ๋žŒ ์ž์ฒด๊ฐ€ ์—†๋‹ค.
# ๋ถ€๋ฌธ๋งˆ๋‹ค ์ง€ํ‘œ๊ฐ€ ๋‹ฌ๋ผ ์ ์ˆ˜๋ฅผ ํ•ฉ์‚ฐํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ 1์œ„ ๋ถ€๋ฌธ์˜ ๊ฐœ์ˆ˜๋กœ ์„ผ๋‹ค.
"rule": "most_first_places",
"require_all_boards": False,
}
def is_host(user):
return (user or "") in HOST_ACCOUNTS
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 {}
# ์ถ• ๊ฐœ์ˆ˜์˜ ์ •๋ณธ์€ ์‹œํ—˜์…‹ ํŒŒ์ผ์ด๊ณ  ์นด๋“œ์—๋Š” ์—†๋‹ค. ์ด ๊ฐ’์„ ์—ฌ๊ธฐ์„œ ์•ˆ ์‹ค์œผ๋ฉด
# ์ œ์ถœ ๊ฒ€์ฆ์ด "ํ™”ํ•ฉ๋ฌผ๋งˆ๋‹ค ์ˆซ์ž ํ•˜๋‚˜"๋ฅผ ์š”๊ตฌํ•ด **์ถ• ๋ถ€๋ฌธ์—๋Š” ์•„๋ฌด๋„ ์ œ์ถœํ•  ์ˆ˜
# ์—†๋‹ค** (2026-09-08 ์ฐธ๊ฐ€์ž ์ œ๋ณด). ํ™”๋ฉด ์•ˆ๋‚ด๋„ ์Šค์นผ๋ผ์šฉ ๋ฌธ๊ตฌ๊ฐ€ ๋œฌ๋‹ค.
# ์นด๋“œ์— ๋„ฃ์ง€ ์•Š๋Š” ์ด์œ : dataset_sha ๊ฐ€ ์นด๋“œ ํ•ด์‹œ๋ผ์„œ, ์นด๋“œ๋ฅผ ๊ณ ์น˜๋ฉด
# ๋ฐ์ดํ„ฐ์…‹์ด ๊ต์ฒด๋œ ๊ฒƒ์ฒ˜๋Ÿผ ๊ธฐ๋ก๋œ๋‹ค.
# mae ๋ถ€๋ฌธ๊นŒ์ง€ ์‹œํ—˜์…‹์„ ์—ด๋ฉด ๋งค ๊ฐฑ์‹ ๋งˆ๋‹ค 46๊ฐœ๋ฅผ ํŒŒ์‹ฑํ•˜๋ฏ€๋กœ ํŠน์ˆ˜ ์ง€ํ‘œ๋งŒ ๋ณธ๋‹ค.
if d.get("axes") is None and d.get("metric") not in (None, "mae"):
tp = os.path.join(DATA, "%s_test.json" % str(d.get("board") or "").lower())
try:
d["axes"] = json.load(io.open(tp, encoding="utf-8")).get("axes")
except Exception:
pass
# ๋ถ„๋ฅ˜ ์ „์šฉ ๋ถ€๋ฌธ์—๋Š” MAE ๊ธฐ์ค€์„ ์ด ์—†๋‹ค. ๊ทธ๋Ÿด ๋•Œ๋Š” AUROC ๊ฐ€ ๊ฐ€์žฅ ๋†’์€ ๊ฒƒ์„ ์ตœ์„ ์œผ๋กœ ๋ณธ๋‹ค.
# ์—†๋Š” ๊ฐ’์„ 0 ์œผ๋กœ ์น˜๋ฉด ๊ทธ ๋ถ€๋ฌธ์ด "๊ฐ€์žฅ ์ •ํ™•ํ•œ ๊ณณ"์œผ๋กœ ํ‘œ์— ์˜ค๋ฅธ๋‹ค.
# ๐Ÿ”ด ์นด๋“œ๊ฐ€ ์ง€์ •ํ•œ ์ฃผ์ง€ํ‘œ๋ฅผ ๋ฎ์–ด์“ฐ์ง€ ์•Š๋Š”๋‹ค. ์—ฌ๊ธฐ์„œ metric ์„ ๋ฌด์กฐ๊ฑด ๋‹ค์‹œ
# ์ •ํ•˜๋Š” ๋ฐ”๋žŒ์—, ํ”„๋กœํŒŒ์ผ ์ƒ๊ด€์œผ๋กœ ์ฑ„์ ๋˜๋Š” ๋ถ€๋ฌธ์ด MAE ๋ถ€๋ฌธ์œผ๋กœ ๋ฐ”๋€Œ์–ด ์žˆ์—ˆ๋‹ค
# (๊ทธ ๋ถ€๋ฌธ์—๋„ MAE ๊ฐ’์ด ๋ถ€์ˆ˜์ ์œผ๋กœ ์‹ค๋ ค ์žˆ์–ด์„œ ์กฐ๊ฑด์— ๊ฑธ๋ ธ๋‹ค). ํ™”๋ฉดยทAPIยท
# ๊ธฐ์ค€์„  ํŒ์ •์ด ์ „๋ถ€ ๊ทธ ๊ฐ’์„ ๋ณด๊ณ  ์žˆ์—ˆ์œผ๋ฏ€๋กœ ํ•œ ์ค„์ด ์„ธ ๊ณณ์„ ๋™์‹œ์— ํ‹€๋ฆฌ๊ฒŒ ํ–ˆ๋‹ค.
card_metric = d.get("metric")
with_mae = {k: v for k, v in base.items() if v.get("mae") is not None}
if card_metric:
best = (max(base, key=lambda k: base[k].get(card_metric) or 0)
if card_metric != "mae"
else (min(with_mae, key=lambda k: with_mae[k]["mae"]) if with_mae else None))
elif 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)
# ํ•™์Šต ๊ธฐ์ค€์„ ์ด ์ƒ์ˆ˜ ์˜ˆ์ธก์„ ์ด๊ธฐ๋Š”๊ฐ€ - **๊ทธ ๋ถ€๋ฌธ์˜ ์ฃผ์ง€ํ‘œ ์œ„์—์„œ** ๋ณธ๋‹ค.
# MAE ๋กœ๋งŒ ๋ณด๋ฉด ์ƒ๊ด€์œผ๋กœ ์ฑ„์ ๋˜๋Š” ๋ถ€๋ฌธ์€ ๋œป ์—†๋Š” ๋น„๊ต๊ฐ€ ๋˜๊ณ , AUROC ์ „์šฉ
# ๋ถ€๋ฌธ์€ mae ๊ฐ€ None ์ด๋ผ ๋น„๊ต ์ž์ฒด๊ฐ€ ์—†์—ˆ๋‹ค.
mk, mhb = primary_metric(d)
tr = (base.get("Morgan+LightGBM") or {}).get(mk)
co = (base.get("์ƒ์ˆ˜ ์˜ˆ์ธก") or {}).get(mk)
if tr is not None and co is not None:
d["beats_constant"] = (tr > co) if mhb else (tr < co)
# ๊ฒฉ์ฐจ๋ฅผ ํ•จ๊ป˜ ์‹ฃ๋Š”๋‹ค. "์ด๊ฒผ๋‹ค/์กŒ๋‹ค"๋งŒ์œผ๋กœ๋Š” 0.003 ๊ณผ 0.27 ์ด ๊ฐ™์•„ ๋ณด์ธ๋‹ค.
d["beats_constant_margin"] = round((tr - co) if mhb else (co - tr), 4)
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 primary_metric(b):
"""์ด ๋ถ€๋ฌธ์˜ ์ฃผ์ง€ํ‘œ์™€ ๋ฐฉํ–ฅ. (key, higher_better)
ํ‘œํ˜„ํ˜•์€ ํ”„๋กœํŒŒ์ผ ์ƒ๊ด€, ๋ถ„๋ฅ˜ ์ „์šฉ์€ AUROC, ๋‚˜๋จธ์ง€๋Š” MAE ๋‹ค. ํŒ์ •์„ ์—ฌ๊ธฐ ํ•œ ๊ณณ์—
๋‘๋Š” ์ด์œ ๋Š” ์ฑ„์ ๊ธฐ์™€ ํ™”๋ฉด์ด ์„œ๋กœ ๋‹ค๋ฅธ ์ง€ํ‘œ๋ฅผ ๋ณด๊ณ  ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค - Phenotype ๋Š”
์ƒ๊ด€์œผ๋กœ ์ฑ„์ ๋˜๋Š”๋ฐ ํ™”๋ฉด์€ MAE ๋งŒ ๋„์›Œ์„œ, ํ•™์Šต ๊ธฐ์ค€์„ ์ด ์ƒ์ˆ˜ ์˜ˆ์ธก์„ 0.003 ์ฐจ์ด๋กœ
๊ฐ„์‹ ํžˆ ์ด๊ธฐ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์˜€๋‹ค(์‹ค์ œ ์ƒ๊ด€์œผ๋กœ๋Š” 0.092 ๋Œ€ 0.159, ์žก์Œ ๋ฐ”๋‹ฅ์˜ 7.6๋ฐฐ).
"""
if (b or {}).get("metric") == "profile_corr":
return "profile_corr", True
bl = (b or {}).get("baselines") or {}
if bl and all(v.get("mae") is None for v in bl.values()) and any(v.get("auroc") is not None for v in bl.values()):
return "auroc", True
return "mae", False
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 submission_count():
"""์›์žฅ์— ๋“ค์–ด์˜จ ์ œ์ถœ ์ˆ˜. **์ฃผ์ตœ์ธก ๊ธฐ์ค€ ์ œ์ถœ๋„ ํฌํ•จํ•œ ์ „์ฒด๋‹ค.**
ํ™”๋ฉด ์ƒ๋‹จ์— ์˜ฌ๋ฆฌ๋Š” ๊ฐ’์ด๋ผ ๋งค ์š”์ฒญ๋งˆ๋‹ค ์›์žฅ์„ ํ›‘์œผ๋ฉด ์•ˆ ๋œ๋‹ค - ํŠธ๋ฆฌ ์กฐํšŒ๋Š”
ํŽ˜์ด์ง€๋‹น 1000๊ฑด์ด๊ณ  ๋ถ€๋ฌธ์ด 46๊ฐœ๋‹ค. 5๋ถ„ ์บ์‹œ๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค: ์ด ์ˆซ์ž๋Š” ์‚ฌ๋žŒ์ด
์ œ์ถœํ•  ๋•Œ๋งŒ ์›€์ง์ด๊ณ , 5๋ถ„ ๋Šฆ๊ฒŒ ๋ณด์ธ๋‹ค๊ณ  ์ž˜๋ชป๋œ ๊ฐ’์ด ๋˜์ง€๋Š” ์•Š๋Š”๋‹ค.
"""
def build():
n, cursor = 0, ""
for _ in range(20): # 20ํŽ˜์ด์ง€ = 2๋งŒ ๊ฑด. ๊ทธ ์œ„๋Š” ์ง€๊ธˆ ์—†๋‹ค.
url = (LEDGER_API + "/tree/main?recursive=1&limit=1000"
+ ("&cursor=" + cursor if cursor else ""))
try:
with urllib.request.urlopen(
urllib.request.Request(url, headers=_hdr()), timeout=60) as r:
rows = json.loads(r.read())
link = r.headers.get("Link") or ""
except Exception:
return None # ๐Ÿ”ด ์‹คํŒจ๋Š” 0 ์ด ์•„๋‹ˆ๋‹ค. ๋ชจ๋ฅด๋Š” ๊ฒƒ์ด๋‹ค.
n += sum(1 for x in rows
if str(x.get("path", "")).startswith("submissions/")
and str(x.get("path", "")).endswith(".json"))
m = re.search(r"cursor=([^&>]+)", link) if 'rel="next"' in link else None
if not m:
break
cursor = m.group(1)
return n
return cached("subcount", 300, build)
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 = []
cover = {} # ๋ˆ„๊ฐ€ ์–ด๋А ๋ถ€๋ฌธ์— ์ œ์ถœํ–ˆ๋‚˜
prize_lead = {} # ๋ถ€๋ฌธ๋ณ„ **์ฐธ๊ฐ€์ž ์ค‘** ์„ ๋‘. ์ƒ๊ธˆ์€ ์ด๊ฒƒ์œผ๋กœ ์„ผ๋‹ค.
for name, b in boards().items():
rolled = ledger_read("leaderboard/%s.json" % name.lower(), {}) or {}
ent = (rolled.get("entries") or [])
# ์ƒ๊ธˆ ์ง‘๊ณ„๋Š” **์ฃผ์ตœ์ธก ์ œ์ถœ์„ ์•„์˜ˆ ์—†๋Š” ๊ฒƒ์œผ๋กœ ๋ณด๊ณ ** ๊ณ„์‚ฐํ•œ๋‹ค.
# ์ฃผ์ตœ์ธก์„ ์ˆ˜์ƒ ๋Œ€์ƒ์—์„œ๋งŒ ๋นผ๋Š” ๊ฒƒ์œผ๋กœ๋Š” ๋ถ€์กฑํ–ˆ๋‹ค - ๊ธฐ์ค€ ์ œ์ถœ์ด 46๋ถ€๋ฌธ
# 1์œ„๋ฅผ ์ „๋ถ€ ์ ์œ ํ•˜์ž ์™ธ๋ถ€ ์ฐธ๊ฐ€์ž์˜ 1์œ„๊ฐ€ 0์ด ๋˜์–ด, ์ƒ๊ธˆ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š”
# ์‚ฌ๋žŒ์ด ๊ตฌ์กฐ์ ์œผ๋กœ ์—†์–ด์กŒ๋‹ค. ํ‘œ์‹œ์ƒ 1์œ„๋Š” ๊ทธ๋Œ€๋กœ ๋‘๊ณ (๋„˜์–ด์•ผ ํ•  ๋ˆˆ๊ธˆ์ด
# ๋ณด์—ฌ์•ผ ํ•œ๋‹ค), ์ƒ๊ธˆ์—์„œ๋Š” ์ฐธ๊ฐ€์ž ์ค‘ ์ตœ์ƒ์œ„๋ฅผ ๊ทธ ๋ถ€๋ฌธ์˜ ์„ ๋‘๋กœ ์„ผ๋‹ค.
for e in ent:
u = e.get("user")
if u and not is_host(u):
cover.setdefault(u, set()).add(name)
lead_p = next((e for e in ent if e.get("user") and not is_host(e.get("user"))), None)
if lead_p:
prize_lead[name] = lead_p.get("user")
top = ent[0] if ent else None
nf = (b.get("noise_floor") or {}).get("sd_single")
base = b.get("baselines") or {}
mkey, mhb = primary_metric(b)
vals = [v.get(mkey) for v in base.values() if v.get(mkey) is not None]
row = {"board": name, "n_test": b.get("n_test"),
"metric": mkey, "higher_better": mhb,
# ์ฃผ์ง€ํ‘œ ์œ„์—์„œ์˜ ์ตœ์„  ๊ธฐ์ค€์„ . ๋„˜์–ด์•ผ ํ•  ์„ ์€ ํ•˜๋‚˜๋ฟ์ด๋‹ค.
"baseline_best_score": (max(vals) if mhb else min(vals)) if vals else None,
"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"),
"host": is_host(top.get("user")),
# score ๋Š” ๊ทธ ๋ถ€๋ฌธ ์ฃผ์ง€ํ‘œ ์œ„์˜ ๊ฐ’์ด๋‹ค. mae ๋Š” ์˜› ํ‘œ์‹œ๋ถ€ ํ˜ธํ™˜.
"score": top.get("score", top.get("mae")),
"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 {"rows": out, "cover": {u: sorted(s) for u, s in cover.items()},
"prize_lead": prize_lead}
got = cached("leaders", 60, build)
rows, cover = got["rows"], got["cover"]
n_boards = len(rows)
# ์ˆ˜์ƒ ํ›„๋ณด. ์ฃผ์ตœ์ธก์€ ์„ธ์ง€ ์•Š๋Š”๋‹ค.
# **์ž๊ฒฉ = ์ „ ๋ถ€๋ฌธ ์ œ์ถœ.** ๋ชป ์ฑ„์šด ์‚ฌ๋žŒ๋„ ํ‘œ์—๋Š” ์˜ฌ๋ฆฐ๋‹ค - ๋ช‡ ๊ฐœ ๋‚จ์•˜๋Š”์ง€ ๋ณด์—ฌ์•ผ
# ์ฑ„์šฐ๋Ÿฌ ์˜จ๋‹ค. ๊ฐ์ถ”๋ฉด ์™œ ์ž๊ธฐ๊ฐ€ ํ›„๋ณด๊ฐ€ ์•„๋‹Œ์ง€ ์•Œ ์ˆ˜ ์—†๋‹ค.
tally = {}
for u in (got.get("prize_lead") or {}).values():
tally[u] = tally.get(u, 0) + 1
# ํ›„๋ณด ๋ชฉ๋ก. 1์œ„๋ฅผ ๊ฐ€์ง„ ์‚ฌ๋žŒ์ด ๋จผ์ € ์˜ค๊ณ , ๊ทธ ๋‹ค์Œ ์ฐธ๊ฐ€ ๋ถ€๋ฌธ ์ˆ˜ ์ˆœ์ด๋‹ค.
# 1์œ„๊ฐ€ ์—†์–ด๋„ ๋ชฉ๋ก์— ๋‚จ๊ธด๋‹ค - ์ฐธ๊ฐ€ํ•œ ์‚ฌ๋žŒ์ด ๋ณด์—ฌ์•ผ ํŒ์ด ์‚ด์•„ ์žˆ๋Š” ๊ฒƒ์œผ๋กœ ์ฝํžŒ๋‹ค.
standing = []
for u, bs in cover.items():
standing.append({"user": u, "firsts": tally.get(u, 0),
"submitted": len(bs), "missing": 0, "eligible": True})
standing.sort(key=lambda x: (-x["firsts"], -x["submitted"], x["user"]))
return {"spec": SPEC, "n": len(rows), "rows": rows,
"submissions": submission_count(),
"held": sum(1 for r in rows if r["leader"]),
"prize": dict(PRIZE, n_boards=n_boards), "standing": standing,
"host_accounts": sorted(HOST_ACCOUNTS)}
@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)
# ์ฃผ์ตœ์ธก์€ ํ•œ๋„๋ฅผ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค. ํ•œ๋„๋Š” ์ฐธ๊ฐ€์ž๋ผ๋ฆฌ์˜ ๊ณต์ •์ด ์•„๋‹ˆ๋ผ **์›์žฅ ๋ถ€ํ•˜**๋ฅผ
# ๋ง‰์œผ๋ ค๊ณ  ์žˆ๋Š” ๊ฒƒ์ด๊ณ , ์ฃผ์ตœ์ธก ์ œ์ถœ์€ ๊ฒจ๋ฃจ๋Š” ์ œ์ถœ์ด ์•„๋‹ˆ๋ผ ๋นˆ ๋ถ€๋ฌธ์— ๋„ฃ๋Š”
# ๋ˆˆ๊ธˆ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค(HOST_ACCOUNTS ๋Š” ์ƒ๊ธˆ ์ง‘๊ณ„์—์„œ ์ด๋ฏธ ๋น ์ง„๋‹ค).
# ํ•œ๋„๋ฅผ ์ง€ํ‚ค๋ฉด 46๋ถ€๋ฌธ์„ ์ฑ„์šฐ๋Š” ๋ฐ๋งŒ ์—ฌ๋Ÿฌ ๋‚ ์ด ๊ฑธ๋ ค, ๊ทธ๋™์•ˆ ์ฐธ๊ฐ€์ž๋Š”
# ๋นˆ ๋ถ€๋ฌธ์—์„œ ๋ฌด์—‡์ด ์ข‹์€ ์ ์ˆ˜์ธ์ง€ ์•Œ ์ˆ˜ ์—†๋‹ค.
if used >= DAILY_CAP and not is_host(u["user"]):
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}