Spaces:
Sleeping
Sleeping
| """chartbench ν΄λ¨Ό μ΄λ°Έ: Qwen vs Opus blind A/B (4μΆ). | |
| - μ μ: 곡μ λ§ν¬μ ?key= κ° ACCESS_KEY μ μΌμΉν΄μΌ νκ° κ°λ₯ (Spaceλ publicμ΄μ§λ§ ν€ μμΌλ©΄ μ§μ λΆκ°) | |
| - ν λΉ: λ―Έμλ£ μμ΄ν μ€ μ΅κ·Ό 10λΆ λ΄ νμΈμκ² ν λΉλμ§ μμ κ²μ λλ€ μ§κΈ. 1λͺ μ μΆ = κ·Έ μμ΄ν μλ£. | |
| - μ μ₯: μ μΆ 1건 = results repo μ JSON 1νμΌ (rater / ν λΉΒ·μ μΆ μκ° / μμμ΄ ν¬ν¨). μ¬μμ μ repo λ‘ μλ£ λͺ©λ‘ 볡μ. | |
| - κ°μ μ΄λ¦ = κ°μ νκ°μλ‘ μ§κ³ (λΆμ μ μ΄λ¦ κΈ°μ€ dedup). | |
| """ | |
| import json, os, time, uuid, random, threading | |
| from datetime import datetime, timezone, timedelta | |
| from pathlib import Path | |
| import gradio as gr | |
| from huggingface_hub import HfApi | |
| ACCESS_KEY = os.environ.get("ACCESS_KEY", "devkey") | |
| RESULTS_REPO = os.environ.get("RESULTS_REPO", "") | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") | |
| RESERVE_TTL = 600 | |
| KST = timezone(timedelta(hours=9)) | |
| ROOT = Path(__file__).parent | |
| ITEMS = [json.loads(l) for l in (ROOT/"items.jsonl").open()] | |
| BY_ID = {it["item_id"]: it for it in ITEMS} | |
| HINT = '<div class="axis-hint">{}</div>' | |
| AXES = [ | |
| ("query_following", | |
| "**1. 쿼리 μꡬμ¬ν λ°μ** β μ΄λ μ°¨νΈκ° μ§λ¬Έμ΄ μꡬν λ°λ₯Ό λ μ λ°μνλμ?"), | |
| ("chart_quality", | |
| "**2. κ°λ μ±Β·μ°¨νΈ νμ§** β μ΄λ μ°¨νΈκ° λ μ½κΈ° μ’κ³ μ μ ν ννμΈκ°μ?"), | |
| ("data_fidelity", | |
| "**3. Data λ°μλ** β μ΄λ μ°¨νΈκ° κΈ°λ° λ°μ΄ν°λ₯Ό λ μ νν λ°μνλμ?" | |
| + HINT.format("λ μ°¨νΈμ μμΉκ° μ μ¬ν΄ 보μ΄λ©΄ 'λΉμ·ν¨'μ λλ¬μ£ΌμΈμ. ν¬κ² λ€λ₯΄λ©΄ νλ¨ ν κΈμ μλ³Έ νλ₯Ό νμΈν΄ νλ¨νμκ³ , ν μμ²΄κ° μ΄μνκ±°λ λ λ€ μλͺ» λ°μνμΌλ©΄ 'λΉμ·ν¨'μ λλ¬μ£ΌμΈμ.")), | |
| ("overall", | |
| "**4. μ λ°μ μ νΈ** β μ’ ν©μ μΌλ‘ μ΄λ μ°¨νΈλ₯Ό μ νΈνμλμ?"), | |
| ] | |
| CHOICES = [("μ°¨νΈ A", "A"), ("μ°¨νΈ B", "B"), ("λΉμ·ν¨ / νλ¨ μ΄λ €μ", "Tie")] | |
| GUIDE = """ | |
| ### μλ΄ | |
| 1. **μ΄λ¦μ μ λ ₯νκ³ μμ**μ λλ₯΄λ©΄ μ νΈ νλ¨ λ¬Έμ κ° λ¬΄μμλ‘ μ μλ©λλ€. *(μ΅λͺ μΌλ‘ μμ±νμ λ λ©λλ€)* | |
| 2. κ° λ¬Έμ λ **μ§λ¬Έ(쿼리) 1κ° + μ°¨νΈ 2κ°(A/B)** λ‘ κ΅¬μ±λλ©°, μ°¨νΈμ κΈ°λ° λ°μ΄ν°(μλ³Έ ν)λ νλ¨μ μ ν μμ΅λλ€. | |
| 3. νλ¨ νλͺ©μ **4κ°μ§** β 쿼리 μꡬμ¬ν λ°μ Β· κ°λ μ± Β· Data λ°μλ Β· μ λ°μ μ νΈ β κ°κ° **A / B / λΉμ·ν¨** μ€ μ νν©λλ€. | |
| 4. μ μΆνλ©΄ κ·Έ λ¬Έμ λ μλ£λκ³ λ€μ λ¬Έμ κ° λμ΅λλ€. **μΈλΉ 10κ° μ΄μ ν΄μ£Όμλ©΄ κ°μ¬νκ² μ΅λλ€.** *(λ°μμλ€λ©΄ λ μ κ² ν΄μ£Όμ λ ν° λμμ΄ λ©λλ€!)* μ€κ°μ κ·Έλ₯ μ°½μ λ«μλ λ©λλ€. | |
| 5. κ°λ³κ² λ³΄κ³ **λκ»΄μ§λ λλ‘** μ ννμλ©΄ λ©λλ€. Data λ°μλλ νλ₯Ό μΌμΌμ΄ λμ‘°ν기보λ€, λ μ°¨νΈμ μμΉκ° ν¬κ² λ€λ₯Ό λλ§ νλ₯Ό μ°Έκ³ νλ μ λλ©΄ μΆ©λΆν©λλ€. | |
| """ | |
| CSS = """ | |
| .guide-box {font-size: 1.15rem; line-height: 1.75;} | |
| .guide-box li {margin-bottom: 6px;} | |
| .axis-hint {color: var(--body-text-color-subdued); font-size: 0.92em; margin-top: 2px;} | |
| .query-card {font-size: 1.25rem; line-height: 1.55; background: var(--block-background-fill); | |
| border: 1px solid var(--border-color-primary); border-left: 6px solid #4f7cff; | |
| border-radius: 10px; padding: 14px 18px; margin: 4px 0 10px;} | |
| .axis-block .wrap {gap: 4px;} | |
| footer {display: none !important;} | |
| """ | |
| api = HfApi(token=HF_TOKEN) if HF_TOKEN else None | |
| _lock = threading.Lock() | |
| _completed: set[str] = set() | |
| _reserved: dict[str, float] = {} | |
| _rater_counts: dict[str, int] = {} | |
| def _load_completed(): | |
| """κ²°κ³Ό repo μ€λ μ·μμ μλ£ μμ΄ν κ³Ό νκ°μλ³ λμ μλ₯Ό 볡μνλ€.""" | |
| if not (api and RESULTS_REPO): | |
| return | |
| try: | |
| from huggingface_hub import snapshot_download | |
| local = snapshot_download(RESULTS_REPO, repo_type="dataset", token=HF_TOKEN, | |
| allow_patterns=["results/*.json"]) | |
| for p in Path(local).glob("results/*.json"): | |
| try: | |
| rec = json.loads(p.read_text()) | |
| except Exception: | |
| continue | |
| if rec.get("item_id"): | |
| _completed.add(rec["item_id"]) | |
| r = (rec.get("rater") or "").strip() | |
| if r: | |
| _rater_counts[r] = _rater_counts.get(r, 0) + 1 | |
| except Exception as e: | |
| print("results repo load failed:", e) | |
| _load_completed() | |
| print(f"items={len(ITEMS)} completed={len(_completed)}") | |
| def _now_iso() -> str: | |
| return datetime.now(KST).isoformat(timespec="seconds") | |
| def _pick_item() -> str | None: | |
| now = time.time() | |
| with _lock: | |
| for k, t in list(_reserved.items()): | |
| if now - t > RESERVE_TTL: | |
| del _reserved[k] | |
| pool = [it["item_id"] for it in ITEMS | |
| if it["item_id"] not in _completed and it["item_id"] not in _reserved] | |
| if not pool: | |
| pool = [it["item_id"] for it in ITEMS if it["item_id"] not in _completed] | |
| if not pool: | |
| return None | |
| item_id = random.choice(pool) | |
| _reserved[item_id] = now | |
| return item_id | |
| def _save(rater: str, item_id: str, answers: dict, assigned_at: str, assigned_ts: float): | |
| rec = {"rater": rater, "item_id": item_id, "answers": answers, | |
| "assigned_at": assigned_at, "submitted_at": _now_iso(), | |
| "elapsed_s": round(time.time() - assigned_ts, 1) if assigned_ts else None, | |
| "id": uuid.uuid4().hex[:8]} | |
| # μ μ₯μ΄ μ±κ³΅ν λ€μλ§ μλ£ μ²λ¦¬νλ€: μ λ‘λ μ€ν¨ μ μμΈλ₯Ό μ¬λ € μ¬μ©μμκ² | |
| # μ¬μ μΆμ μλ΄νκ³ , μμ΄ν μ λ―Έμλ£λ‘ λ¨λλ€ (μλ΅ μ μ€ λ°©μ§). | |
| if api and RESULTS_REPO: | |
| num = item_id.split("_")[1] | |
| api.upload_file(path_or_fileobj=json.dumps(rec, ensure_ascii=False).encode(), | |
| path_in_repo=f"results/r_{num}_{rec['id']}.json", | |
| repo_id=RESULTS_REPO, repo_type="dataset", | |
| commit_message=f"rating {item_id} by {rater}") | |
| else: | |
| with (ROOT/"local_results.jsonl").open("a") as f: | |
| f.write(json.dumps(rec, ensure_ascii=False)+"\n") | |
| with _lock: | |
| _completed.add(item_id) | |
| _reserved.pop(item_id, None) | |
| _rater_counts[rater] = _rater_counts.get(rater, 0) + 1 | |
| def _progress(my_count: int) -> str: | |
| return f"μ 체 μ§ν **{len(_completed)} / {len(ITEMS)}** Β· λ΄κ° νκ°ν μ **{my_count}**" | |
| def _render_item(item_id: str): | |
| it = BY_ID[item_id] | |
| return (f'<div class="query-card"><b>μ§λ¬Έ</b><br>{it["query"]}</div>', | |
| str(ROOT/"images"/f"{item_id}_A.png"), str(ROOT/"images"/f"{item_id}_B.png"), | |
| it["table_markdown"]) | |
| N_START_OUT = 12 # gate, panel, query, imgA, imgB, table, progress, rater, item, assigned_at, assigned_ts, my_count | |
| def start(rater, request: gr.Request): | |
| if (request.query_params.get("key") or "") != ACCESS_KEY: | |
| raise gr.Error("μ κ·Ό ν€κ° μμ΅λλ€. 곡μ λ°μ λ§ν¬λ‘ μ μνμΈμ.") | |
| rater = (rater or "").strip() | |
| if not rater: | |
| gr.Warning("μ΄λ¦(λλ μ΅λͺ μ½λ)μ μ λ ₯ν΄μ£ΌμΈμ.") | |
| return tuple(gr.skip() for _ in range(N_START_OUT)) | |
| my_count = _rater_counts.get(rater, 0) | |
| item_id = _pick_item() | |
| if item_id is None: | |
| gr.Info("λͺ¨λ λ¬Έμ νκ°κ° λλ¬μ΅λλ€. κ°μ¬ν©λλ€!") | |
| return (gr.update(visible=True), gr.update(visible=False), "", None, None, "", | |
| "π λͺ¨λ λ¬Έμ κ° μλ£λμμ΅λλ€!", rater, "", "", 0.0, my_count) | |
| q, a, b, t = _render_item(item_id) | |
| return (gr.update(visible=False), gr.update(visible=True), q, a, b, t, | |
| _progress(my_count), rater, item_id, _now_iso(), time.time(), my_count) | |
| N_SUB_OUT = 11 # query, imgA, imgB, table, progress, item, assigned_at, assigned_ts, my_count, + 4 radios -> actually computed below | |
| def submit(rater, item_id, assigned_at, assigned_ts, my_count, c1, c2, c3, c4, request: gr.Request): | |
| choices = (c1, c2, c3, c4) | |
| n_out = 9 + len(AXES) | |
| if (request.query_params.get("key") or "") != ACCESS_KEY: | |
| raise gr.Error("μ κ·Ό ν€κ° μμ΅λλ€.") | |
| if not item_id: | |
| gr.Warning("ν λΉλ λ¬Έμ κ° μμ΅λλ€. μλ‘κ³ μΉ¨ ν λ€μ μμν΄μ£ΌμΈμ.") | |
| return tuple(gr.skip() for _ in range(n_out)) | |
| missing = [f"{i+1}λ²" for i, c in enumerate(choices) if c is None] | |
| if missing: | |
| gr.Warning("λͺ¨λ νλͺ©μ μ νν΄μ£ΌμΈμ. λ―Έμ ν: " + ", ".join(missing)) | |
| return tuple(gr.skip() for _ in range(n_out)) | |
| try: | |
| _save(rater, item_id, {k: c for (k, _), c in zip(AXES, choices)}, assigned_at, assigned_ts) | |
| except Exception as e: | |
| print("save failed:", type(e).__name__, e) | |
| gr.Warning("μ μ₯μ μ€ν¨νμ΅λλ€ (λ€νΈμν¬ λ¬Έμ μΌ μ μμ΅λλ€). μ μ ν 'μ μΆνκ³ λ€μ'μ λ€μ λλ¬μ£ΌμΈμ β μ νμ μ μ§λ©λλ€.") | |
| with _lock: | |
| _reserved[item_id] = time.time() # μ΄ μ¬μ©μ λͺ«μΌλ‘ μμ½ μ°μ₯ | |
| return tuple(gr.skip() for _ in range(n_out)) | |
| my_count = (my_count or 0) + 1 | |
| nxt = _pick_item() | |
| if nxt is None: | |
| return ("π λͺ¨λ λ¬Έμ κ° μλ£λμμ΅λλ€. μ°Έμ¬ν΄μ£Όμ μ κ°μ¬ν©λλ€!", None, None, "", | |
| _progress(my_count), "", "", 0.0, my_count, *[None]*len(AXES)) | |
| q, a, b, t = _render_item(nxt) | |
| return (q, a, b, t, _progress(my_count), nxt, _now_iso(), time.time(), my_count, *[None]*len(AXES)) | |
| with gr.Blocks(title="μ°¨νΈ μ νΈ νκ°") as demo: | |
| gr.Markdown("# π μ°¨νΈ μμ± μ νΈ νκ° (Blind A/B)\nκ°μ μ§λ¬ΈΒ·κ°μ λ°μ΄ν°λ‘ λ μμ€ν μ΄ λ§λ μ°¨νΈμ λλ€. νλͺ©λ³λ‘ **λ λμ μͺ½**μ 골λΌμ£ΌμΈμ. A/B λ°°μΉλ λ¬Έμ λ§λ€ 무μμμ λλ€.") | |
| with gr.Group(visible=True) as gate: | |
| rater_in = gr.Textbox(label="μ΄λ¦ (λλ μ΅λͺ μ½λ)", placeholder="μ: λ°OO") | |
| start_btn = gr.Button("νκ° μμ", variant="primary") | |
| gr.Markdown(GUIDE, elem_classes="guide-box") | |
| with gr.Group(visible=False) as panel: | |
| prog = gr.Markdown() | |
| qhtml = gr.HTML() | |
| with gr.Row(): | |
| img_a = gr.Image(label="μ°¨νΈ A", type="filepath", height=440) | |
| img_b = gr.Image(label="μ°¨νΈ B", type="filepath", height=440) | |
| radios = [] | |
| for _, axis_md in AXES: | |
| gr.Markdown(axis_md) | |
| radios.append(gr.Radio(CHOICES, show_label=False, elem_classes="axis-block")) | |
| sub_btn = gr.Button("μ μΆνκ³ λ€μ β", variant="primary", size="lg") | |
| with gr.Accordion("μλ³Έ ν νΌμ³λ³΄κΈ° (Data λ°μλ μ°Έκ³ μ©)", open=False): | |
| tbl = gr.Markdown() | |
| rater_st = gr.State(""); item_st = gr.State(""); at_st = gr.State(""); ts_st = gr.State(0.0); cnt_st = gr.State(0) | |
| start_btn.click(start, [rater_in], | |
| [gate, panel, qhtml, img_a, img_b, tbl, prog, rater_st, item_st, at_st, ts_st, cnt_st]) | |
| sub_btn.click(submit, [rater_st, item_st, at_st, ts_st, cnt_st, *radios], | |
| [qhtml, img_a, img_b, tbl, prog, item_st, at_st, ts_st, cnt_st, *radios]) | |
| if __name__ == "__main__": | |
| demo.launch(css=CSS) | |