Spaces:
Sleeping
Sleeping
Download column_view.py from dev-strender/proofread-demo: direct link, hf CLI and curl.
- Browser
- Download file 15.2 kB
-
https://huggingface.co/spaces/dev-strender/proofread-demo/resolve/main/column_view.py
- Command line
-
hf download hf://spaces/dev-strender/proofread-demo/column_view.py
-
curl -L -o column_view.py https://huggingface.co/spaces/dev-strender/proofread-demo/resolve/main/column_view.py
15.2 kB
| """κ΅μ΄ μ§νΒ·κ²°κ³Ό ν μ΄μ 그리λ νλ©΄ μ‘°κ° (Streamlit) - λ‘컬 3μ’ λΉκ΅ μ±(`proofread-demo/compare_app.py`)κ³Ό κ°μ λͺ¨μ. | |
| κΈ°λ€λ¦¬λ λμ "λ€μμ 무μμ΄ μ§νλλμ§"κ° λ³΄μ΄λλ‘: | |
| - μμ½ 3μΉΈ(μμ Β· μ μ© Β· νμΈ νμ)κ³Ό λ¨κ³ μ€ν νΌ(λλ λ¨κ³ β μμ μκ° Β· μ§ν μ€ β κ²½κ³Ό/λ³΄ν΅ Nμ΄ Β· λκΈ° β) | |
| - λ¨κ³κ° λλ λλ§λ€ λ³Έλ¬Έ νμ΄λΌμ΄νΈΒ·μμ μΉ΄λκ° κ·Έ μ리μμ μμΈλ€ | |
| - μΉ΄λΒ·νμ΄λΌμ΄νΈλ κ²°κ³Ό λ³Έλ¬Έμ λ¨μ λ³κ²½λ§ 보μΈλ€(FT κ° κ³ μΉκ³ κΈ°μ¬ κ²ν κ° μλ¬ΈμΌλ‘ λλλ¦° κ²μ λ λ€ λΊλ€) | |
| μ΄λ²€νΈ λͺ¨μμ `runner.start_proofread` λ₯Ό λ°λ₯Έλ€. | |
| """ | |
| from __future__ import annotations | |
| import difflib | |
| import html | |
| import time | |
| from typing import Any | |
| import streamlit as st | |
| # νλ©΄μ λ³΄μΌ λ¨κ³. (νμ μ΄λ¦, κ·Έ λ¨κ³κ° λλ¬λ€κ³ 보λ μ€ν μ΄λ¦, λ³΄ν΅ κ±Έλ¦¬λ μ΄) | |
| STAGES = [ | |
| ("νκΈ° μ¬μ μ μ©", "vocabulary_sub_pre", 0.1), | |
| ("κ΅μ΄ λͺ¨λΈ(FT) λ¬Έλ¨λ³", "proofread", 1), | |
| ("κΈ°μ¬ μ 체 κ²ν (1ν)", "review", 7), | |
| ("FT κ΅μ νμΈ", "revert_ft", 0.1), | |
| ("μ¬μ€Β·μ«μΒ·λ¬Έλ§₯ κ΅μ μ μ©", "apply_review", 0.1), | |
| ("λ§λ¬΄λ¦¬ κ·μΉ", "post_process", 0.1), | |
| ] | |
| # κ΅μ λͺ©λ‘μ μ μ©νλ μ€ν : (λͺ©λ‘μ λΈ μ€ν , μ«μ κ°λ, μμ κ°λ λΉμ¨) - μΉ΄λ μνλ₯Ό μ μ© μ€ν κ³Ό κ°μ κ°λλ‘ κ³μ°νλ€ | |
| LIST_CARDS = {"apply_review": ("review", False, 0.6)} | |
| # FT λ³κ²½ λλλ¦Ό: (λλλ¦Ό μ€ν , λ³κ²½ λͺ©λ‘ μ€ν , νμ μ€ν ) | |
| REVERTS = ("revert_ft", "ft_hunks", "review") | |
| STAGE_COLORS = [ | |
| "rgba(120,120,120,.25)", | |
| "rgba(70,130,220,.28)", | |
| "rgba(60,170,90,.28)", | |
| "rgba(240,150,40,.32)", | |
| "rgba(170,90,200,.28)", | |
| "rgba(120,120,120,.25)", | |
| ] | |
| CAT_KO = { | |
| "spelling": "λ§μΆ€λ²", | |
| "spacing": "λμ΄μ°κΈ°Β·μ¬μ΄μμ·", | |
| "grammar": "μ‘°μ¬Β·νΈμ", | |
| "context": "λ¬Έλ§₯ λ±λ§", | |
| "omission": "λλ½", | |
| "in-article": "κΈ°μ¬ μ λΆμΌμΉ", | |
| "inconsistency": "κΈ°μ¬ μ λΆμΌμΉ", | |
| "fact": "μ¬μ€ μ€κΈ°", | |
| "calc": "μ«μ κ²μ°", | |
| "word": "λ¬Έλ§₯ λ±λ§", | |
| "unit": "λ¨μΒ·μμΉ λͺ¨μ", | |
| } | |
| CSS = """ | |
| <style> | |
| .pane{border:1px solid rgba(128,128,128,.35);border-radius:8px;padding:12px 14px;line-height:1.95;font-size:.98rem;min-height:120px} | |
| .shimmer{height:4px;border-radius:2px;margin:6px 0 10px;background:linear-gradient(90deg,rgba(128,128,128,.15) 25%,rgba(128,128,128,.45) 50%,rgba(128,128,128,.15) 75%);background-size:200% 100%;animation:sh 1.2s linear infinite} | |
| @keyframes sh{0%{background-position:200% 0}100%{background-position:-200% 0}} | |
| .stage{font-size:.9rem;margin:2px 0} | |
| .card{border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:6px 10px;margin-bottom:6px;font-size:.9rem} | |
| .muted{color:gray;font-size:.82rem} | |
| .stats{display:flex;gap:8px;margin:4px 0 8px} | |
| .stat{flex:1;border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:6px 8px;text-align:center} | |
| .stat .k{font-size:.72rem;color:gray}.stat .v{font-size:1.05rem;font-weight:700} | |
| .status{min-height:30px;margin-bottom:6px} | |
| .chip{display:inline-block;font-size:.75rem;padding:1px 8px;border-radius:10px;margin-left:6px;font-weight:600} | |
| .ok{background:rgba(60,170,90,.22);color:#1e7d3a} | |
| .chk{background:rgba(240,170,40,.28);color:#8a5a00} | |
| .rej{background:rgba(128,128,128,.22);color:#555} | |
| </style> | |
| """ | |
| NL = "\n" # μ€λ°κΏμ μ΄μ νλλ‘ λ€λ€ diffΒ·νμ΄λΌμ΄νΈκ° μ λ ₯μ μ€ κ΅¬μ‘°λ₯Ό 보쑴νκ² νλ€ | |
| def tokenize(text: str) -> list[str]: | |
| """μ΄μ + μ€λ°κΏ ν ν°. λΉ μ€μ NL ν ν°μ΄ μ°μμΌλ‘ λ¨μ λ¬Έλ¨ κ°κ²©μ΄ μ μ§λλ€.""" | |
| out: list[str] = [] | |
| for line in text.split("\n"): | |
| out.extend(line.split()) | |
| out.append(NL) | |
| return out[:-1] | |
| def _norm(s: str) -> str: | |
| return " ".join(s.split()) | |
| # βββββββββββββββββββββββββ μν βββββββββββββββββββββββββ | |
| def new_state(original: str) -> dict[str, Any]: | |
| return { | |
| "original": original, | |
| "stage_i": 0, | |
| "stage_done_at": {}, | |
| "tokens": None, | |
| "last_text": None, | |
| "cards": [], | |
| "log": "λκΈ° μ€", | |
| "done": None, | |
| "error": None, | |
| "started": time.monotonic(), | |
| } | |
| def _label_tokens(state: dict, new_text: str, stage_i: int) -> None: | |
| """μλ¬Έ μ΄μ μ 'μ΄λ λ¨κ³μμ λ°λμλ' λΌλ²¨μ λμ νλ€. μλ¬Έκ³Ό κ°μμ§ μ΄μ μ λΌλ²¨μ μ§μ΄λ€.""" | |
| if state["tokens"] is None: | |
| state["tokens"] = [(t, -1) for t in tokenize(state["last_text"])] | |
| old = [t for t, _ in state["tokens"]] | |
| new = tokenize(new_text) | |
| sm = difflib.SequenceMatcher(a=old, b=new, autojunk=False) | |
| out: list[tuple[str, int]] = [] | |
| for op, i1, i2, j1, j2 in sm.get_opcodes(): | |
| out.extend(state["tokens"][i1:i2] if op == "equal" else ((t, stage_i) for t in new[j1:j2])) | |
| orig = tokenize(state["original"]) | |
| net = difflib.SequenceMatcher(a=orig, b=[t for t, _ in out], autojunk=False) | |
| for op, _i1, _i2, j1, j2 in net.get_opcodes(): | |
| if op == "equal": | |
| out[j1:j2] = [(t, -1) for t, _ in out[j1:j2]] | |
| state["tokens"] = out | |
| state["last_text"] = new_text | |
| def _item_status( | |
| item: dict, text_before: str, protect_digits: bool, max_shrink: float | None | |
| ) -> str: | |
| from solar_eval.pipelines.corrections import apply_corrections | |
| _, rep = apply_corrections( | |
| text_before, [item], max_items=1, protect_digits=protect_digits, max_shrink=max_shrink | |
| ) | |
| if rep.applied: | |
| return "μ μ©" | |
| if rep.skipped_protected: | |
| return "κ±°λΆ (μ«μΒ·μμ κ°λ)" | |
| if rep.skipped_ambiguous or rep.skipped_not_found: | |
| return "νμΈ νμ" | |
| return "무ν¨" | |
| def _diff_card(stage: int, before: str, after: str) -> dict: | |
| spacing = "".join(before.split()) == "".join(after.split()) | |
| return { | |
| "stage": stage, | |
| "before": before, | |
| "after": after, | |
| "cat": "λμ΄μ°κΈ°" if spacing else "", | |
| "status": "μ μ©", | |
| "evidence": "", | |
| } | |
| def _add_card(state: dict, card: dict) -> None: | |
| """μΉ΄λλ₯Ό λνλ€. μ λ¨κ³μ μ μ© μΉ΄λλ₯Ό κ·Έλλ‘ λλ리λ λ³κ²½μ΄λ©΄ λ λ€ μ§μ΄λ€(κ²°κ³Όμ μΌλ‘ λ°λ κ²μ΄ μλ€).""" | |
| if card["status"] == "μ μ©": | |
| for i, c in enumerate(state["cards"]): | |
| if ( | |
| c["status"] == "μ μ©" | |
| and _norm(c["before"]) == _norm(card["after"]) | |
| and _norm(c["after"]) == _norm(card["before"]) | |
| ): | |
| del state["cards"][i] | |
| return | |
| state["cards"].append(card) | |
| def _drop_reverted(state: dict, ev: dict) -> None: | |
| """κΈ°μ¬ κ²ν κ° μλ¬ΈμΌλ‘ λλλ¦° FT λ³κ²½μ μΉ΄λμμ μ§μ΄λ€.""" | |
| _step, hunks_from, judge_from = REVERTS | |
| decision = ev["json"].get(judge_from) or {} | |
| by_id = {int(h["id"]): h for h in ev["hunks"].get(hunks_from) or []} | |
| for raw in decision.get("revert") or []: | |
| h = by_id.get(int(raw)) if str(raw).isdigit() else None | |
| if h is None: | |
| continue | |
| pair = (_norm(h["before"]), _norm(h["after"])) | |
| state["cards"] = [ | |
| c | |
| for c in state["cards"] | |
| if not (c["status"] == "μ μ©" and (_norm(c["before"]), _norm(c["after"])) == pair) | |
| ] | |
| def absorb(state: dict, ev: dict) -> None: | |
| """μ΄λ²€νΈ νλλ₯Ό μνμ λ°μνλ€. λ¨κ³ μλ£ νμ Β·μΉ΄λΒ·νμ΄λΌμ΄νΈΒ·λ‘κ·Έ.""" | |
| if ev["kind"] == "error": | |
| state["error"] = ev["error"] | |
| state["log"] = "μ€ν¨" | |
| return | |
| if ev["kind"] == "done": | |
| state["done"] = ev | |
| state["log"] = f"μλ£ - {ev['elapsed_s']:.1f}μ΄" | |
| return | |
| step, text = ev["step"], ev["text"] | |
| stage_steps = [s[1] for s in STAGES] | |
| si = state["stage_i"] # μ΄ μ€ν μ λ€μ λ―Έμ λ¨κ³μ μνλ€ | |
| if state["last_text"] is None: | |
| state["last_text"] = state["original"] | |
| pre_text = state["last_text"] | |
| list_card = LIST_CARDS.get(step) | |
| reverted = False | |
| if list_card and list_card[0] in ev["corrections"]: | |
| corr_key, protect_digits, max_shrink = list_card | |
| for it in ev["corrections"][corr_key]: | |
| cat = str(it.get("category", "")) | |
| _add_card( | |
| state, | |
| { | |
| "stage": si, | |
| "before": str(it.get("original", "")), | |
| "after": str(it.get("corrected", "")), | |
| "cat": CAT_KO.get(cat, cat), | |
| "status": _item_status(it, pre_text, protect_digits, max_shrink), | |
| "evidence": str(it.get("evidence", "")), | |
| }, | |
| ) | |
| elif step == REVERTS[0]: | |
| _drop_reverted(state, ev) | |
| reverted = True | |
| elif ev["changes"] and not list_card: | |
| for b, a in ev["changes"]: | |
| _add_card(state, _diff_card(si, b, a)) | |
| if text != pre_text or state["tokens"] is None: | |
| _label_tokens(state, text, si) | |
| if step in stage_steps: | |
| idx = stage_steps.index(step) | |
| state["stage_done_at"][idx] = ev["elapsed_s"] | |
| state["stage_i"] = idx + 1 | |
| n = sum(c["stage"] == idx for c in state["cards"]) | |
| n_chk = sum(c["stage"] == idx and c["status"] == "νμΈ νμ" for c in state["cards"]) | |
| extra = ( | |
| (f" - μ μ {n}건" + (f" (νμΈ νμ {n_chk})" if n_chk else "")) | |
| if n | |
| else " - κ³ μΉ κ³³ μμ" | |
| ) | |
| if reverted: | |
| extra = "" # νμΈ λ¨κ³λ μ§μ΄ μΉ΄λ μλ₯Ό λ΄μΈμ°μ§ μλλ€ | |
| state["log"] = f"λ°©κΈ: {STAGES[idx][0]} μλ£{extra}" | |
| # βββββββββββββββββββββββββ λ λ βββββββββββββββββββββββββ | |
| def _render_text(state: dict) -> str: | |
| toks = state["tokens"] or [(t, -1) for t in tokenize(state["original"])] | |
| parts: list[str] = [] | |
| run: list[str] = [] | |
| cur = -1 | |
| def flush() -> None: | |
| if run: | |
| seg = html.escape(" ".join(run)) | |
| parts.append( | |
| seg | |
| if cur < 0 | |
| else f'<span style="background:{STAGE_COLORS[cur % len(STAGE_COLORS)]};border-radius:3px">{seg}</span>' | |
| ) | |
| run.clear() | |
| for t, s in toks + [("", -2)]: | |
| if t == NL: # μ λ ₯μ μ€λ°κΏμ κ·Έλλ‘ - λΉ μ€μ <br> λ λ² | |
| flush() | |
| parts.append("<br>") | |
| continue | |
| if s != cur: | |
| flush() | |
| cur = s | |
| if t: | |
| run.append(t) | |
| return ( | |
| " ".join(parts).replace(" <br> ", "<br>").replace("<br> ", "<br>").replace(" <br>", "<br>") | |
| ) | |
| def _cards_html(state: dict) -> str: | |
| cards = [c for c in state["cards"] if _norm(c["before"]) != _norm(c["after"])] | |
| if not cards: | |
| return "" | |
| parts = [] | |
| for c in cards: | |
| color = STAGE_COLORS[c["stage"] % len(STAGE_COLORS)] | |
| if c["status"] == "μ μ©": | |
| cls, label = "ok", "β μ μ©" | |
| elif c["status"] == "νμΈ νμ": | |
| cls, label = "chk", "νμΈ νμ" | |
| else: | |
| cls, label = "rej", c["status"] | |
| ev_html = ( | |
| f"<div class='muted'>κ·Όκ±°: {html.escape(c['evidence'])}</div>" if c["evidence"] else "" | |
| ) | |
| parts.append( | |
| f"<div class='card' style='border-left:5px solid {color}'><span class='muted'>" | |
| f"{STAGES[c['stage']][0]}{' Β· ' + c['cat'] if c['cat'] else ''}</span>" | |
| f"<span class='chip {cls}'>{label}</span><br>" | |
| f"<s>{html.escape(c['before'])}</s> β <b>{html.escape(c['after'])}</b>{ev_html}</div>" | |
| ) | |
| return f"<div class='muted' style='margin-top:6px'>μ μ {len(cards)}건</div>" + "".join(parts) | |
| def render(ph: Any, state: dict, now: float) -> None: | |
| """μΌμͺ½: μμ½ 3μΉΈ Β· λ¨κ³ μ€ν νΌ Β· μν μ€ / μ€λ₯Έμͺ½: νμ΄λΌμ΄νΈ λ³Έλ¬Έ Β· μμ μΉ΄λ.""" | |
| elapsed = now - state["started"] | |
| expected = sum(s[2] for s in STAGES) | |
| with ph.container(): | |
| left, right = st.columns([2, 3], gap="large") | |
| with left: | |
| n_ok = sum(c["status"] == "μ μ©" for c in state["cards"]) | |
| n_chk = sum(c["status"] == "νμΈ νμ" for c in state["cards"]) | |
| if state["error"]: | |
| took = "μ€ν¨" | |
| elif state["done"]: | |
| took = f"{state['done']['elapsed_s']:.1f}μ΄" | |
| else: | |
| took = f"{elapsed:.0f}μ΄β¦" | |
| st.markdown( | |
| "<div class='stats'>" | |
| f"<div class='stat'><div class='k'>μμ</div><div class='v'>{took}</div></div>" | |
| f"<div class='stat'><div class='k'>μ μ©</div><div class='v'>{n_ok}</div></div>" | |
| f"<div class='stat'><div class='k'>νμΈ νμ</div><div class='v'>{n_chk}</div></div>" | |
| "</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| lines = [] | |
| for i, (name, _step, typical) in enumerate(STAGES): | |
| prev = state["stage_done_at"].get(i - 1, 0.0) | |
| if i in state["stage_done_at"]: | |
| lines.append( | |
| f"<div class='stage'>β {name} <span class='muted'>({state['stage_done_at'][i] - prev:.1f}μ΄)</span></div>" | |
| ) | |
| elif i == state["stage_i"] and not state["done"] and not state["error"]: | |
| lines.append( | |
| f"<div class='stage'>β <b>{name}</b> <span class='muted'>μ§ν μ€ {elapsed - prev:.0f}μ΄ Β· λ³΄ν΅ {typical:.0f}μ΄</span></div>" | |
| ) | |
| else: | |
| lines.append(f"<div class='stage muted'>β {name}</div>") | |
| st.markdown("".join(lines), unsafe_allow_html=True) | |
| if state["error"]: | |
| st.markdown( | |
| f"<div class='status' style='color:#b00020'>μ€ν¨: {html.escape(state['error'])}</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| elif state["done"]: | |
| st.markdown( | |
| f"<div class='status' style='color:#1e7d3a'>μλ£ {state['done']['elapsed_s']:.1f}μ΄</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| else: | |
| msg = state["log"] | |
| if elapsed > expected * 1.5 + 5: | |
| msg += f" Β· νμ({expected:.0f}μ΄)λ³΄λ€ μ€λ 걸립λλ€ - API λκΈ° μ€μΌ μ μμ΅λλ€. κ²°κ³Όλ κ·Έλλ‘ λμ°©ν©λλ€." | |
| st.markdown( | |
| f"<div class='status muted'>{elapsed:.0f}μ΄ κ²½κ³Ό Β· {msg}<div class='shimmer'></div></div>", | |
| unsafe_allow_html=True, | |
| ) | |
| with right: | |
| st.markdown(f"<div class='pane'>{_render_text(state)}</div>", unsafe_allow_html=True) | |
| # μΉ΄λλ ν λ©μ΄λ¦¬ HTML λ‘ κ·Έλ¦°λ€ - μΉ΄λλ§λ€ st.markdown μ λΆλ₯΄λ©΄ μΉ΄λκ° μ€μμ λ μ§λ λ λμ μΉ΄λκ° λ¨λλ€ | |
| st.markdown(_cards_html(state), unsafe_allow_html=True) | |