Spaces:
Sleeping
Sleeping
feat: progress stepper + highlighted result + correction cards (local compare view)
Browse files- __pycache__/column_view.cpython-312.pyc +0 -0
- __pycache__/runner.cpython-312.pyc +0 -0
- app.py +54 -116
- column_view.py +356 -0
- runner.py +68 -42
__pycache__/column_view.cpython-312.pyc
ADDED
|
Binary file (19.3 kB). View file
|
|
|
__pycache__/runner.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/runner.cpython-312.pyc and b/__pycache__/runner.cpython-312.pyc differ
|
|
|
app.py
CHANGED
|
@@ -2,27 +2,23 @@
|
|
| 2 |
|
| 3 |
FT ๊ต์ด + Opus ๊ธฐ์ฌ ๊ฒํ ๊ตฌ์ฑ์ solar-eval ์์ง ๊ทธ๋๋ก ์คํํ๋ค - FT ๊ต์ด ๋ชจ๋ธ์ด ๋ฌธ๋จ ๋ฌถ์๋ง๋ค 1์ฐจ ๊ต์ดํ๊ณ ,
|
| 4 |
Claude Opus ๊ฐ ๊ธฐ์ฌ ์ ์ฒด๋ฅผ ํ ๋ฒ ๋ณด๊ณ FT ์ค๊ต์ ์ ๋๋๋ฆฌ๋ฉฐ ์ฌ์คยท์ซ์ยท๋ฌธ๋งฅ ์ค๋ฅ๋ฅผ ๊ณ ์น ๋ค ๊ฒฐ์ ๋ก ๊ท์น์ ๊ฑด๋ค.
|
| 5 |
-
๊ตฌ์ฑ ์ ์์ ์คํ์
|
| 6 |
-
Supabase ์ ๋จ๊ธด๋ค(์ํฌ๋ฆฟ์ด ์์ผ๋ฉด ์กฐ์ฉํ ๊ฑด๋๋).
|
| 7 |
"""
|
| 8 |
|
| 9 |
from __future__ import annotations
|
| 10 |
|
| 11 |
-
import difflib
|
| 12 |
-
import html
|
| 13 |
import os
|
|
|
|
|
|
|
| 14 |
|
|
|
|
| 15 |
import logbook
|
| 16 |
import runner
|
| 17 |
import streamlit as st
|
| 18 |
|
| 19 |
-
PANE_STYLE = (
|
| 20 |
-
"border:1px solid rgba(128,128,128,.35);border-radius:8px;"
|
| 21 |
-
"padding:14px 16px;line-height:1.9;font-size:1.02rem;min-height:120px"
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
# ํฉ์ฑ ์๋ฌธ - ํ๊ฐ์
gold ๋ ์ฐ์ง ์๋๋ค(์ ์ถ ๋ฐฉ์ง). ๊ฐ ์ธต์ ํ์ ์ ์ผ๋ถ๋ฌ ์ฌ์ด ๋ ๋ฌธ์ฅ์ด๋ค:
|
| 25 |
-
# ๋น๋ฅ /๊ฐฑ์ ๋ฅ /์ฐ์ฒด๋ฅ (์ฌ์ ยท์จ๋ฅ ๊ท์น) ยท
|
| 26 |
SAMPLE_TEXT = (
|
| 27 |
"โถ์ ๋ถ์ ๊ฐ๋ ฅํ ๋ถ๋์ฐ ๊ท์ ์๋ ๋ถ๊ตฌํ๊ณ ์๋๊ถ ์ํํธ ๋งค๋งค ๊ฑฐ๋ ๋น๋ฅ ์ "
|
| 28 |
"์คํ๋ ค ์ฆ๊ฐ์ธ๋ฅผ ๋ณด์๋ค. ์์ค ์ํ์ ์ฃผํ ๋ด๋ณด ๋์ถ ๊ธ๋ฆฌ๊ฐ ๋ฎ์์ง๋ฉด์ "
|
|
@@ -32,77 +28,6 @@ SAMPLE_TEXT = (
|
|
| 32 |
"์ฃผ๋ชฉํ๊ณ ์๋ค. ์ ๊ท ๊ด์ญ์ฒ ๋๋ ์ต๊ณ ์์ 160ใ์ผ๋ก ์ค๊ณ๋๋ค."
|
| 33 |
)
|
| 34 |
|
| 35 |
-
# ์คํ
์ด๋ฆ โ ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ค ๋ผ๋ฒจ (๋ชจ๋ฅด๋ ์คํ
์ ์ด๋ฆ ๊ทธ๋๋ก)
|
| 36 |
-
STEP_LABELS = {
|
| 37 |
-
"vocabulary_sub_pre": "์ฉ์ด ์ฌ์ ์นํ",
|
| 38 |
-
"proofread": "1์ฐจ ๊ต์ด (๊ต์ด ์ ์ฉ FT ๋ชจ๋ธ, ๋ฌธ๋จ๋ณ)",
|
| 39 |
-
"ft_hunks": "FT ์์ ๋ชฉ๋ก ์ ๋ฆฌ",
|
| 40 |
-
"review": "๊ธฐ์ฌ ์ ์ฒด ๊ฒํ (Claude Opus 1ํ)",
|
| 41 |
-
"revert_ft": "FT ์ค๊ต์ ๋๋๋ฆผ",
|
| 42 |
-
"apply_review": "์ฌ์คยท์ซ์ยท๋ฌธ๋งฅ ๊ต์ ์ ์ฉ",
|
| 43 |
-
"quote_direction": "๋ฐ์ดํ ๋ฐฉํฅ ์ ๋ฆฌ",
|
| 44 |
-
"quote_preserve": "์๋ฌธ ๋ฐ์ดํ ๋ณด์กด",
|
| 45 |
-
"yul_ryul_normalize": "์จ/๋ฅ ๊ท์น ์ ์ฉ",
|
| 46 |
-
"post_process": "ํ์ฒ๋ฆฌ (๋จ์ยทํตํยท์ผํ)",
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def diff_panes(src: str, dst: str) -> tuple[str, str, int]:
|
| 51 |
-
"""์ด์ ๋จ์ diff ๋ฅผ ์ข(์๋ฌธ)/์ฐ(๊ต์ ) ํจ๋ HTML ๋ก. ๋ฐ๋ ์ธ๊ทธ๋จผํธ ์๋ ๋ฐํ."""
|
| 52 |
-
a, b = src.split(), dst.split()
|
| 53 |
-
sm = difflib.SequenceMatcher(a=a, b=b, autojunk=False)
|
| 54 |
-
left: list[str] = []
|
| 55 |
-
right: list[str] = []
|
| 56 |
-
n_changed = 0
|
| 57 |
-
for op, i1, i2, j1, j2 in sm.get_opcodes():
|
| 58 |
-
before, after = html.escape(" ".join(a[i1:i2])), html.escape(" ".join(b[j1:j2]))
|
| 59 |
-
if op == "equal":
|
| 60 |
-
left.append(before)
|
| 61 |
-
right.append(after)
|
| 62 |
-
continue
|
| 63 |
-
n_changed += 1
|
| 64 |
-
if before:
|
| 65 |
-
left.append(f'<span style="background:rgba(230,80,80,.20)">{before}</span>')
|
| 66 |
-
if after:
|
| 67 |
-
right.append(f'<span style="background:rgba(60,170,90,.24)">{after}</span>')
|
| 68 |
-
return " ".join(left), " ".join(right), n_changed
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
class ProgressView:
|
| 72 |
-
"""์คํ
์งํ์ st.status ์์ ๊ทธ๋ฆฐ๋ค - ๋จ๊ณ๋ณ ์ถ๋ ฅ์ ๋ณด์ฌ์ฃผ์ง ์๊ณ '์ด๋๊น์ง ์๋' ๋ง."""
|
| 73 |
-
|
| 74 |
-
def __init__(self, names: list[str]) -> None:
|
| 75 |
-
self.names = names
|
| 76 |
-
self.done = 0
|
| 77 |
-
self.status = st.status("๊ต์ด ์ค์
๋๋คโฆ", expanded=True)
|
| 78 |
-
self.bar = self.status.progress(0.0, text=f"0 / {len(names)} ๋จ๊ณ")
|
| 79 |
-
self.lines = self.status.empty()
|
| 80 |
-
self._render()
|
| 81 |
-
|
| 82 |
-
def on_step(self, event: str, index: int, total: int, name: str) -> None:
|
| 83 |
-
if event == "done":
|
| 84 |
-
self.done = index
|
| 85 |
-
self.bar.progress(min(self.done / total, 1.0), text=f"{self.done} / {total} ๋จ๊ณ")
|
| 86 |
-
self._render()
|
| 87 |
-
|
| 88 |
-
def _render(self) -> None:
|
| 89 |
-
rows = []
|
| 90 |
-
for i, name in enumerate(self.names, 1):
|
| 91 |
-
label = STEP_LABELS.get(name, name)
|
| 92 |
-
if i <= self.done:
|
| 93 |
-
rows.append(f"โ
{label}")
|
| 94 |
-
elif i == self.done + 1:
|
| 95 |
-
rows.append(f"โณ **{label}**")
|
| 96 |
-
else:
|
| 97 |
-
rows.append(f"โซ๏ธ {label}")
|
| 98 |
-
self.lines.markdown(" \n".join(rows))
|
| 99 |
-
|
| 100 |
-
def finish(self, elapsed_s: float) -> None:
|
| 101 |
-
self.status.update(label=f"๊ต์ด ์๋ฃ ยท {elapsed_s:.1f}์ด", state="complete", expanded=False)
|
| 102 |
-
|
| 103 |
-
def fail(self) -> None:
|
| 104 |
-
self.status.update(label="๊ต์ด ์คํจ", state="error", expanded=True)
|
| 105 |
-
|
| 106 |
|
| 107 |
@st.cache_resource(show_spinner="ํ์ดํ๋ผ์ธ ์ค๋น ์คโฆ")
|
| 108 |
def get_pipeline():
|
|
@@ -110,8 +35,9 @@ def get_pipeline():
|
|
| 110 |
|
| 111 |
|
| 112 |
st.set_page_config(page_title="์กฐ์ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์ด ๋ฐ๋ชจ", page_icon="๐ฐ", layout="wide")
|
|
|
|
| 113 |
st.title("๐ฐ ์กฐ์ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์ด ๋ฐ๋ชจ")
|
| 114 |
-
st.caption(f"{runner.CONFIG.label} ยท
|
| 115 |
|
| 116 |
with st.sidebar:
|
| 117 |
st.subheader("์ค์ ")
|
|
@@ -125,48 +51,60 @@ with st.sidebar:
|
|
| 125 |
cfg = runner.CONFIG
|
| 126 |
st.markdown(f"**{cfg.label}**")
|
| 127 |
st.caption(cfg.note)
|
| 128 |
-
st.markdown(f"- ํ์ดํ๋ผ์ธ `{cfg.pipeline}`\n- ํ๋กฌํํธ `{cfg.prompt}`")
|
| 129 |
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
-
if
|
|
|
|
| 133 |
missing = [k for k in runner.REQUIRED_KEYS if not os.environ.get(k)]
|
|
|
|
|
|
|
|
|
|
| 134 |
if missing:
|
| 135 |
st.error(
|
| 136 |
f"{', '.join(missing)} ๊ฐ ํ์ํฉ๋๋ค - ์ฌ์ด๋๋ฐ์ ์
๋ ฅํ๊ฑฐ๋ Space Secrets ์ ์ค์ ํ์ธ์."
|
| 137 |
)
|
| 138 |
st.stop()
|
| 139 |
-
|
| 140 |
-
source = text.strip()
|
| 141 |
-
pipeline = get_pipeline()
|
| 142 |
-
# ์
๋ ฅ์ ์คํ ์ ์ ๋จ๊ธด๋ค - ์คํ์ด ์คํจํด๋ ๋ฌด์์ด ๋ค์ด์๋์ง๋ ์ ์ ์๊ฒ
|
| 143 |
-
article_id = logbook.save_article(source)
|
| 144 |
-
|
| 145 |
-
progress = ProgressView(runner.step_names(pipeline))
|
| 146 |
try:
|
| 147 |
-
|
| 148 |
-
except Exception as e: # noqa: BLE001 - ๋ฐ๋ชจ ํ๋ฉด
|
| 149 |
-
|
| 150 |
-
st.error(f"์คํ ์คํจ: {type(e).__name__}: {e}")
|
| 151 |
st.stop()
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
logbook.save_run(
|
| 155 |
-
article_id,
|
| 156 |
-
pipeline_key=result["pipeline_key"],
|
| 157 |
-
prompt_key=result["prompt_key"],
|
| 158 |
-
model=result["model"],
|
| 159 |
-
output=result["output"],
|
| 160 |
-
processing_time_s=result["elapsed_s"],
|
| 161 |
-
)
|
| 162 |
-
if logbook.is_configured() and logbook.last_error():
|
| 163 |
-
st.caption(f"๊ธฐ๋ก ์คํจ (๊ฒฐ๊ณผ์๋ ์ํฅ ์์): {logbook.last_error()}")
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
FT ๊ต์ด + Opus ๊ธฐ์ฌ ๊ฒํ ๊ตฌ์ฑ์ solar-eval ์์ง ๊ทธ๋๋ก ์คํํ๋ค - FT ๊ต์ด ๋ชจ๋ธ์ด ๋ฌธ๋จ ๋ฌถ์๋ง๋ค 1์ฐจ ๊ต์ดํ๊ณ ,
|
| 4 |
Claude Opus ๊ฐ ๊ธฐ์ฌ ์ ์ฒด๋ฅผ ํ ๋ฒ ๋ณด๊ณ FT ์ค๊ต์ ์ ๋๋๋ฆฌ๋ฉฐ ์ฌ์คยท์ซ์ยท๋ฌธ๋งฅ ์ค๋ฅ๋ฅผ ๊ณ ์น ๋ค ๊ฒฐ์ ๋ก ๊ท์น์ ๊ฑด๋ค.
|
| 5 |
+
๊ตฌ์ฑ ์ ์์ ์คํ์ runner.py, ์งํยท๊ฒฐ๊ณผ ํ๋ฉด์ column_view.py(๋ก์ปฌ 3์ข
๋น๊ต ์ฑ๊ณผ ๊ฐ์ ๋ชจ์)์ด๊ณ ์ด ํ์ผ์ ํ์ด์ง๋ง
|
| 6 |
+
๋ด๋นํ๋ค. ์ฌ์ฉ์ ์
๋ ฅ๊ณผ ๊ฒฐ๊ณผ๋ logbook.py ๊ฐ Supabase ์ ๋จ๊ธด๋ค(์ํฌ๋ฆฟ์ด ์์ผ๋ฉด ์กฐ์ฉํ ๊ฑด๋๋).
|
| 7 |
"""
|
| 8 |
|
| 9 |
from __future__ import annotations
|
| 10 |
|
|
|
|
|
|
|
| 11 |
import os
|
| 12 |
+
import queue
|
| 13 |
+
import time
|
| 14 |
|
| 15 |
+
import column_view as cv
|
| 16 |
import logbook
|
| 17 |
import runner
|
| 18 |
import streamlit as st
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
# ํฉ์ฑ ์๋ฌธ - ํ๊ฐ์
gold ๋ ์ฐ์ง ์๋๋ค(์ ์ถ ๋ฐฉ์ง). ๊ฐ ์ธต์ ํ์ ์ ์ผ๋ถ๋ฌ ์ฌ์ด ๋ ๋ฌธ์ฅ์ด๋ค:
|
| 21 |
+
# ๋น๋ฅ /๊ฐฑ์ ๋ฅ /์ฐ์ฒด๋ฅ (์ฌ์ ยท์จ๋ฅ ๊ท์น) ยท 160ใ์ผ๋ก(์กฐ์ฌ) ยท ๊ณง์ ๋ฐ์ดํ(์๋ฌธ ๋ฐ์ดํ ๋ณด์กด).
|
| 22 |
SAMPLE_TEXT = (
|
| 23 |
"โถ์ ๋ถ์ ๊ฐ๋ ฅํ ๋ถ๋์ฐ ๊ท์ ์๋ ๋ถ๊ตฌํ๊ณ ์๋๊ถ ์ํํธ ๋งค๋งค ๊ฑฐ๋ ๋น๋ฅ ์ "
|
| 24 |
"์คํ๋ ค ์ฆ๊ฐ์ธ๋ฅผ ๋ณด์๋ค. ์์ค ์ํ์ ์ฃผํ ๋ด๋ณด ๋์ถ ๊ธ๋ฆฌ๊ฐ ๋ฎ์์ง๋ฉด์ "
|
|
|
|
| 28 |
"์ฃผ๋ชฉํ๊ณ ์๋ค. ์ ๊ท ๊ด์ญ์ฒ ๋๋ ์ต๊ณ ์์ 160ใ์ผ๋ก ์ค๊ณ๋๋ค."
|
| 29 |
)
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
@st.cache_resource(show_spinner="ํ์ดํ๋ผ์ธ ์ค๋น ์คโฆ")
|
| 33 |
def get_pipeline():
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
st.set_page_config(page_title="์กฐ์ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์ด ๋ฐ๋ชจ", page_icon="๐ฐ", layout="wide")
|
| 38 |
+
st.markdown(cv.CSS, unsafe_allow_html=True)
|
| 39 |
st.title("๐ฐ ์กฐ์ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์ด ๋ฐ๋ชจ")
|
| 40 |
+
st.caption(f"{runner.CONFIG.label} ยท ๋จ๊ณ๊ฐ ๋๋๋ ๋๋ก ๊ฒฐ๊ณผ๊ฐ ์์
๋๋ค")
|
| 41 |
|
| 42 |
with st.sidebar:
|
| 43 |
st.subheader("์ค์ ")
|
|
|
|
| 51 |
cfg = runner.CONFIG
|
| 52 |
st.markdown(f"**{cfg.label}**")
|
| 53 |
st.caption(cfg.note)
|
|
|
|
| 54 |
|
| 55 |
+
# ํผ ์์ ์ฌ๋ฌ ์ค ์
๋ ฅ์ โ+Enter(๋งฅ) ยท Ctrl+Enter ๋ก ํผ์ ์ ์ถํ๋ค - ๋ฒํผ์ ๋๋ฅด์ง ์์๋ ๋ฐ๋ก ๊ต์ด์ ์์ํ๋ค.
|
| 56 |
+
with st.form("article_form", border=False):
|
| 57 |
+
text = st.text_area("๊ธฐ์ฌ ๋ณธ๋ฌธ", value=SAMPLE_TEXT, height=220, max_chars=12000)
|
| 58 |
+
go = st.form_submit_button("๊ต์ด ์คํ (โ/Ctrl + Enter)", type="primary")
|
| 59 |
+
ph = st.empty()
|
| 60 |
|
| 61 |
+
if go:
|
| 62 |
+
source = text.strip()
|
| 63 |
missing = [k for k in runner.REQUIRED_KEYS if not os.environ.get(k)]
|
| 64 |
+
if not source:
|
| 65 |
+
st.warning("๊ธฐ์ฌ ๋ณธ๋ฌธ์ ๋ฃ์ด ์ฃผ์ธ์.")
|
| 66 |
+
st.stop()
|
| 67 |
if missing:
|
| 68 |
st.error(
|
| 69 |
f"{', '.join(missing)} ๊ฐ ํ์ํฉ๋๋ค - ์ฌ์ด๋๋ฐ์ ์
๋ ฅํ๊ฑฐ๋ Space Secrets ์ ์ค์ ํ์ธ์."
|
| 70 |
)
|
| 71 |
st.stop()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
try:
|
| 73 |
+
pipeline = get_pipeline()
|
| 74 |
+
except Exception as e: # noqa: BLE001 - ๋ฐ๋ชจ ํ๋ฉด: ์์ธ๋ง ํ๋ฉด์ผ๋ก
|
| 75 |
+
st.error(f"ํ์ดํ๋ผ์ธ ์ค๋น ์คํจ: {type(e).__name__}: {e}")
|
|
|
|
| 76 |
st.stop()
|
| 77 |
+
# ์
๋ ฅ์ ์คํ ์ ์ ๋จ๊ธด๋ค - ์คํ์ด ์คํจํด๋ ๋ฌด์์ด ๋ค์ด์๋์ง๋ ์ ์ ์๊ฒ
|
| 78 |
+
article_id = logbook.save_article(source)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
+
state = cv.new_state(source)
|
| 81 |
+
events = runner.start_proofread(pipeline, source)
|
| 82 |
+
cv.render(ph, state, time.monotonic())
|
| 83 |
+
last_draw = 0.0
|
| 84 |
+
while not (state["done"] or state["error"]):
|
| 85 |
+
moved = False
|
| 86 |
+
try:
|
| 87 |
+
while True:
|
| 88 |
+
cv.absorb(state, events.get_nowait())
|
| 89 |
+
moved = True
|
| 90 |
+
except queue.Empty:
|
| 91 |
+
pass
|
| 92 |
+
now = time.monotonic()
|
| 93 |
+
if moved or now - last_draw > 0.5:
|
| 94 |
+
cv.render(ph, state, now)
|
| 95 |
+
last_draw = now
|
| 96 |
+
if not moved:
|
| 97 |
+
time.sleep(0.15)
|
| 98 |
+
cv.render(ph, state, time.monotonic())
|
| 99 |
+
|
| 100 |
+
if state["done"]:
|
| 101 |
+
logbook.save_run(
|
| 102 |
+
article_id,
|
| 103 |
+
pipeline_key=cfg.pipeline,
|
| 104 |
+
prompt_key=cfg.prompt,
|
| 105 |
+
model=cfg.model,
|
| 106 |
+
output=state["done"]["output"],
|
| 107 |
+
processing_time_s=state["done"]["elapsed_s"],
|
| 108 |
+
)
|
| 109 |
+
if logbook.is_configured() and logbook.last_error():
|
| 110 |
+
st.caption(f"๊ธฐ๋ก ์คํจ (๊ฒฐ๊ณผ์๋ ์ํฅ ์์): {logbook.last_error()}")
|
column_view.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""๊ต์ด ์งํยท๊ฒฐ๊ณผ ํ ์ด์ ๊ทธ๋ฆฌ๋ ํ๋ฉด ์กฐ๊ฐ (Streamlit) - ๋ก์ปฌ 3์ข
๋น๊ต ์ฑ(`proofread-demo/compare_app.py`)๊ณผ ๊ฐ์ ๋ชจ์.
|
| 2 |
+
|
| 3 |
+
๊ธฐ๋ค๋ฆฌ๋ ๋์ "๋ค์์ ๋ฌด์์ด ์งํ๋๋์ง"๊ฐ ๋ณด์ด๋๋ก:
|
| 4 |
+
- ์์ฝ 3์นธ(์์ ยท ์ ์ฉ ยท ํ์ธ ํ์)๊ณผ ๋จ๊ณ ์คํ
ํผ(๋๋ ๋จ๊ณ โ ์์ ์๊ฐ ยท ์งํ ์ค โ ๊ฒฝ๊ณผ/๋ณดํต N์ด ยท ๋๊ธฐ โ)
|
| 5 |
+
- ๋จ๊ณ๊ฐ ๋๋ ๋๋ง๋ค ๋ณธ๋ฌธ ํ์ด๋ผ์ดํธยท์์ ์นด๋๊ฐ ๊ทธ ์๋ฆฌ์์ ์์ธ๋ค
|
| 6 |
+
- ์นด๋ยทํ์ด๋ผ์ดํธ๋ ๊ฒฐ๊ณผ ๋ณธ๋ฌธ์ ๋จ์ ๋ณ๊ฒฝ๋ง ๋ณด์ธ๋ค(FT ๊ฐ ๊ณ ์น๊ณ ๊ธฐ์ฌ ๊ฒํ ๊ฐ ์๋ฌธ์ผ๋ก ๋๋๋ฆฐ ๊ฒ์ ๋ ๋ค ๋บ๋ค)
|
| 7 |
+
์ด๋ฒคํธ ๋ชจ์์ `runner.start_proofread` ๋ฅผ ๋ฐ๋ฅธ๋ค.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import difflib
|
| 13 |
+
import html
|
| 14 |
+
import time
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
import streamlit as st
|
| 18 |
+
|
| 19 |
+
# ํ๋ฉด์ ๋ณด์ผ ๋จ๊ณ. (ํ์ ์ด๋ฆ, ๊ทธ ๋จ๊ณ๊ฐ ๋๋ฌ๋ค๊ณ ๋ณด๋ ์คํ
์ด๋ฆ, ๋ณดํต ๊ฑธ๋ฆฌ๋ ์ด)
|
| 20 |
+
STAGES = [
|
| 21 |
+
("ํ๊ธฐ ์ฌ์ ์ ์ฉ", "vocabulary_sub_pre", 0.1),
|
| 22 |
+
("๊ต์ด ๋ชจ๋ธ(FT) ๋ฌธ๋จ๋ณ", "proofread", 1),
|
| 23 |
+
("๊ธฐ์ฌ ์ ์ฒด ๊ฒํ (Opus 1ํ)", "review", 7),
|
| 24 |
+
("FT ๊ต์ ํ์ธ", "revert_ft", 0.1),
|
| 25 |
+
("์ฌ์คยท์ซ์ยท๋ฌธ๋งฅ ๊ต์ ์ ์ฉ", "apply_review", 0.1),
|
| 26 |
+
("๋ง๋ฌด๋ฆฌ ๊ท์น", "post_process", 0.1),
|
| 27 |
+
]
|
| 28 |
+
# ๊ต์ ๋ชฉ๋ก์ ์ ์ฉํ๋ ์คํ
: (๋ชฉ๋ก์ ๋ธ ์คํ
, ์ซ์ ๊ฐ๋, ์ญ์ ๊ฐ๋ ๋น์จ) - ์นด๋ ์ํ๋ฅผ ์ ์ฉ ์คํ
๊ณผ ๊ฐ์ ๊ฐ๋๋ก ๊ณ์ฐํ๋ค
|
| 29 |
+
LIST_CARDS = {"apply_review": ("review", False, 0.6)}
|
| 30 |
+
# FT ๋ณ๊ฒฝ ๋๋๋ฆผ: (๋๋๋ฆผ ์คํ
, ๋ณ๊ฒฝ ๋ชฉ๋ก ์คํ
, ํ์ ์คํ
)
|
| 31 |
+
REVERTS = ("revert_ft", "ft_hunks", "review")
|
| 32 |
+
|
| 33 |
+
STAGE_COLORS = [
|
| 34 |
+
"rgba(120,120,120,.25)",
|
| 35 |
+
"rgba(70,130,220,.28)",
|
| 36 |
+
"rgba(60,170,90,.28)",
|
| 37 |
+
"rgba(240,150,40,.32)",
|
| 38 |
+
"rgba(170,90,200,.28)",
|
| 39 |
+
"rgba(120,120,120,.25)",
|
| 40 |
+
]
|
| 41 |
+
CAT_KO = {
|
| 42 |
+
"spelling": "๋ง์ถค๋ฒ",
|
| 43 |
+
"spacing": "๋์ด์ฐ๊ธฐยท์ฌ์ด์์ท",
|
| 44 |
+
"grammar": "์กฐ์ฌยทํธ์",
|
| 45 |
+
"context": "๋ฌธ๋งฅ ๋ฑ๋ง",
|
| 46 |
+
"omission": "๋๋ฝ",
|
| 47 |
+
"in-article": "๊ธฐ์ฌ ์ ๋ถ์ผ์น",
|
| 48 |
+
"inconsistency": "๊ธฐ์ฌ ์ ๋ถ์ผ์น",
|
| 49 |
+
"fact": "์ฌ์ค ์ค๊ธฐ",
|
| 50 |
+
"calc": "์ซ์ ๊ฒ์ฐ",
|
| 51 |
+
"word": "๋ฌธ๋งฅ ๋ฑ๋ง",
|
| 52 |
+
"unit": "๋จ์ยท์์น ๋ชจ์",
|
| 53 |
+
}
|
| 54 |
+
CSS = """
|
| 55 |
+
<style>
|
| 56 |
+
.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}
|
| 57 |
+
.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}
|
| 58 |
+
@keyframes sh{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
| 59 |
+
.stage{font-size:.9rem;margin:2px 0}
|
| 60 |
+
.card{border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:6px 10px;margin-bottom:6px;font-size:.9rem}
|
| 61 |
+
.muted{color:gray;font-size:.82rem}
|
| 62 |
+
.stats{display:flex;gap:8px;margin:4px 0 8px}
|
| 63 |
+
.stat{flex:1;border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:6px 8px;text-align:center}
|
| 64 |
+
.stat .k{font-size:.72rem;color:gray}.stat .v{font-size:1.05rem;font-weight:700}
|
| 65 |
+
.status{min-height:30px;margin-bottom:6px}
|
| 66 |
+
.chip{display:inline-block;font-size:.75rem;padding:1px 8px;border-radius:10px;margin-left:6px;font-weight:600}
|
| 67 |
+
.ok{background:rgba(60,170,90,.22);color:#1e7d3a}
|
| 68 |
+
.chk{background:rgba(240,170,40,.28);color:#8a5a00}
|
| 69 |
+
.rej{background:rgba(128,128,128,.22);color:#555}
|
| 70 |
+
</style>
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
NL = "\n" # ์ค๋ฐ๊ฟ์ ์ด์ ํ๋๋ก ๋ค๋ค diffยทํ์ด๋ผ์ดํธ๊ฐ ์
๋ ฅ์ ์ค ๊ตฌ์กฐ๋ฅผ ๋ณด์กดํ๊ฒ ํ๋ค
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def tokenize(text: str) -> list[str]:
|
| 77 |
+
"""์ด์ + ์ค๋ฐ๊ฟ ํ ํฐ. ๋น ์ค์ NL ํ ํฐ์ด ์ฐ์์ผ๋ก ๋จ์ ๋ฌธ๋จ ๊ฐ๊ฒฉ์ด ์ ์ง๋๋ค."""
|
| 78 |
+
out: list[str] = []
|
| 79 |
+
for line in text.split("\n"):
|
| 80 |
+
out.extend(line.split())
|
| 81 |
+
out.append(NL)
|
| 82 |
+
return out[:-1]
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _norm(s: str) -> str:
|
| 86 |
+
return " ".join(s.split())
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโ ์ํ โโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 90 |
+
def new_state(original: str) -> dict[str, Any]:
|
| 91 |
+
return {
|
| 92 |
+
"original": original,
|
| 93 |
+
"stage_i": 0,
|
| 94 |
+
"stage_done_at": {},
|
| 95 |
+
"tokens": None,
|
| 96 |
+
"last_text": None,
|
| 97 |
+
"cards": [],
|
| 98 |
+
"log": "๋๊ธฐ ์ค",
|
| 99 |
+
"done": None,
|
| 100 |
+
"error": None,
|
| 101 |
+
"started": time.monotonic(),
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _label_tokens(state: dict, new_text: str, stage_i: int) -> None:
|
| 106 |
+
"""์๋ฌธ ์ด์ ์ '์ด๋ ๋จ๊ณ์์ ๋ฐ๋์๋' ๋ผ๋ฒจ์ ๋์ ํ๋ค. ์๋ฌธ๊ณผ ๊ฐ์์ง ์ด์ ์ ๋ผ๋ฒจ์ ์ง์ด๋ค."""
|
| 107 |
+
if state["tokens"] is None:
|
| 108 |
+
state["tokens"] = [(t, -1) for t in tokenize(state["last_text"])]
|
| 109 |
+
old = [t for t, _ in state["tokens"]]
|
| 110 |
+
new = tokenize(new_text)
|
| 111 |
+
sm = difflib.SequenceMatcher(a=old, b=new, autojunk=False)
|
| 112 |
+
out: list[tuple[str, int]] = []
|
| 113 |
+
for op, i1, i2, j1, j2 in sm.get_opcodes():
|
| 114 |
+
out.extend(state["tokens"][i1:i2] if op == "equal" else ((t, stage_i) for t in new[j1:j2]))
|
| 115 |
+
orig = tokenize(state["original"])
|
| 116 |
+
net = difflib.SequenceMatcher(a=orig, b=[t for t, _ in out], autojunk=False)
|
| 117 |
+
for op, _i1, _i2, j1, j2 in net.get_opcodes():
|
| 118 |
+
if op == "equal":
|
| 119 |
+
out[j1:j2] = [(t, -1) for t, _ in out[j1:j2]]
|
| 120 |
+
state["tokens"] = out
|
| 121 |
+
state["last_text"] = new_text
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _item_status(
|
| 125 |
+
item: dict, text_before: str, protect_digits: bool, max_shrink: float | None
|
| 126 |
+
) -> str:
|
| 127 |
+
from solar_eval.pipelines.corrections import apply_corrections
|
| 128 |
+
|
| 129 |
+
_, rep = apply_corrections(
|
| 130 |
+
text_before, [item], max_items=1, protect_digits=protect_digits, max_shrink=max_shrink
|
| 131 |
+
)
|
| 132 |
+
if rep.applied:
|
| 133 |
+
return "์ ์ฉ"
|
| 134 |
+
if rep.skipped_protected:
|
| 135 |
+
return "๊ฑฐ๋ถ (์ซ์ยท์ญ์ ๊ฐ๋)"
|
| 136 |
+
if rep.skipped_ambiguous or rep.skipped_not_found:
|
| 137 |
+
return "ํ์ธ ํ์"
|
| 138 |
+
return "๋ฌดํจ"
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _diff_card(stage: int, before: str, after: str) -> dict:
|
| 142 |
+
spacing = "".join(before.split()) == "".join(after.split())
|
| 143 |
+
return {
|
| 144 |
+
"stage": stage,
|
| 145 |
+
"before": before,
|
| 146 |
+
"after": after,
|
| 147 |
+
"cat": "๋์ด์ฐ๊ธฐ" if spacing else "",
|
| 148 |
+
"status": "์ ์ฉ",
|
| 149 |
+
"evidence": "",
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def _add_card(state: dict, card: dict) -> None:
|
| 154 |
+
"""์นด๋๋ฅผ ๋ํ๋ค. ์ ๋จ๊ณ์ ์ ์ฉ ์นด๋๋ฅผ ๊ทธ๋๋ก ๋๋๋ฆฌ๋ ๋ณ๊ฒฝ์ด๋ฉด ๋ ๋ค ์ง์ด๋ค(๊ฒฐ๊ณผ์ ์ผ๋ก ๋ฐ๋ ๊ฒ์ด ์๋ค)."""
|
| 155 |
+
if card["status"] == "์ ์ฉ":
|
| 156 |
+
for i, c in enumerate(state["cards"]):
|
| 157 |
+
if (
|
| 158 |
+
c["status"] == "์ ์ฉ"
|
| 159 |
+
and _norm(c["before"]) == _norm(card["after"])
|
| 160 |
+
and _norm(c["after"]) == _norm(card["before"])
|
| 161 |
+
):
|
| 162 |
+
del state["cards"][i]
|
| 163 |
+
return
|
| 164 |
+
state["cards"].append(card)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def _drop_reverted(state: dict, ev: dict) -> None:
|
| 168 |
+
"""๊ธฐ์ฌ ๊ฒํ ๊ฐ ์๋ฌธ์ผ๋ก ๋๋๋ฆฐ FT ๋ณ๊ฒฝ์ ์นด๋์์ ์ง์ด๋ค."""
|
| 169 |
+
_step, hunks_from, judge_from = REVERTS
|
| 170 |
+
decision = ev["json"].get(judge_from) or {}
|
| 171 |
+
by_id = {int(h["id"]): h for h in ev["hunks"].get(hunks_from) or []}
|
| 172 |
+
for raw in decision.get("revert") or []:
|
| 173 |
+
h = by_id.get(int(raw)) if str(raw).isdigit() else None
|
| 174 |
+
if h is None:
|
| 175 |
+
continue
|
| 176 |
+
pair = (_norm(h["before"]), _norm(h["after"]))
|
| 177 |
+
state["cards"] = [
|
| 178 |
+
c
|
| 179 |
+
for c in state["cards"]
|
| 180 |
+
if not (c["status"] == "์ ์ฉ" and (_norm(c["before"]), _norm(c["after"])) == pair)
|
| 181 |
+
]
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def absorb(state: dict, ev: dict) -> None:
|
| 185 |
+
"""์ด๋ฒคํธ ํ๋๋ฅผ ์ํ์ ๋ฐ์ํ๋ค. ๋จ๊ณ ์๋ฃ ํ์ ยท์นด๋ยทํ์ด๋ผ์ดํธยท๋ก๊ทธ."""
|
| 186 |
+
if ev["kind"] == "error":
|
| 187 |
+
state["error"] = ev["error"]
|
| 188 |
+
state["log"] = "์คํจ"
|
| 189 |
+
return
|
| 190 |
+
if ev["kind"] == "done":
|
| 191 |
+
state["done"] = ev
|
| 192 |
+
state["log"] = f"์๋ฃ - {ev['elapsed_s']:.1f}์ด"
|
| 193 |
+
return
|
| 194 |
+
step, text = ev["step"], ev["text"]
|
| 195 |
+
stage_steps = [s[1] for s in STAGES]
|
| 196 |
+
si = state["stage_i"] # ์ด ์คํ
์ ๋ค์ ๋ฏธ์ ๋จ๊ณ์ ์ํ๋ค
|
| 197 |
+
if state["last_text"] is None:
|
| 198 |
+
state["last_text"] = state["original"]
|
| 199 |
+
pre_text = state["last_text"]
|
| 200 |
+
list_card = LIST_CARDS.get(step)
|
| 201 |
+
reverted = False
|
| 202 |
+
if list_card and list_card[0] in ev["corrections"]:
|
| 203 |
+
corr_key, protect_digits, max_shrink = list_card
|
| 204 |
+
for it in ev["corrections"][corr_key]:
|
| 205 |
+
cat = str(it.get("category", ""))
|
| 206 |
+
_add_card(
|
| 207 |
+
state,
|
| 208 |
+
{
|
| 209 |
+
"stage": si,
|
| 210 |
+
"before": str(it.get("original", "")),
|
| 211 |
+
"after": str(it.get("corrected", "")),
|
| 212 |
+
"cat": CAT_KO.get(cat, cat),
|
| 213 |
+
"status": _item_status(it, pre_text, protect_digits, max_shrink),
|
| 214 |
+
"evidence": str(it.get("evidence", "")),
|
| 215 |
+
},
|
| 216 |
+
)
|
| 217 |
+
elif step == REVERTS[0]:
|
| 218 |
+
_drop_reverted(state, ev)
|
| 219 |
+
reverted = True
|
| 220 |
+
elif ev["changes"] and not list_card:
|
| 221 |
+
for b, a in ev["changes"]:
|
| 222 |
+
_add_card(state, _diff_card(si, b, a))
|
| 223 |
+
if text != pre_text or state["tokens"] is None:
|
| 224 |
+
_label_tokens(state, text, si)
|
| 225 |
+
if step in stage_steps:
|
| 226 |
+
idx = stage_steps.index(step)
|
| 227 |
+
state["stage_done_at"][idx] = ev["elapsed_s"]
|
| 228 |
+
state["stage_i"] = idx + 1
|
| 229 |
+
n = sum(c["stage"] == idx for c in state["cards"])
|
| 230 |
+
n_chk = sum(c["stage"] == idx and c["status"] == "ํ์ธ ํ์" for c in state["cards"])
|
| 231 |
+
extra = (
|
| 232 |
+
(f" - ์ ์ {n}๊ฑด" + (f" (ํ์ธ ํ์ {n_chk})" if n_chk else ""))
|
| 233 |
+
if n
|
| 234 |
+
else " - ๊ณ ์น ๊ณณ ์์"
|
| 235 |
+
)
|
| 236 |
+
if reverted:
|
| 237 |
+
extra = "" # ํ์ธ ๋จ๊ณ๋ ์ง์ด ์นด๋ ์๋ฅผ ๋ด์ธ์ฐ์ง ์๋๋ค
|
| 238 |
+
state["log"] = f"๋ฐฉ๊ธ: {STAGES[idx][0]} ์๋ฃ{extra}"
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโ ๋ ๋ โโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 242 |
+
def _render_text(state: dict) -> str:
|
| 243 |
+
toks = state["tokens"] or [(t, -1) for t in tokenize(state["original"])]
|
| 244 |
+
parts: list[str] = []
|
| 245 |
+
run: list[str] = []
|
| 246 |
+
cur = -1
|
| 247 |
+
|
| 248 |
+
def flush() -> None:
|
| 249 |
+
if run:
|
| 250 |
+
seg = html.escape(" ".join(run))
|
| 251 |
+
parts.append(
|
| 252 |
+
seg
|
| 253 |
+
if cur < 0
|
| 254 |
+
else f'<span style="background:{STAGE_COLORS[cur % len(STAGE_COLORS)]};border-radius:3px">{seg}</span>'
|
| 255 |
+
)
|
| 256 |
+
run.clear()
|
| 257 |
+
|
| 258 |
+
for t, s in toks + [("", -2)]:
|
| 259 |
+
if t == NL: # ์
๋ ฅ์ ์ค๋ฐ๊ฟ์ ๊ทธ๋๋ก - ๋น ์ค์ <br> ๋ ๋ฒ
|
| 260 |
+
flush()
|
| 261 |
+
parts.append("<br>")
|
| 262 |
+
continue
|
| 263 |
+
if s != cur:
|
| 264 |
+
flush()
|
| 265 |
+
cur = s
|
| 266 |
+
if t:
|
| 267 |
+
run.append(t)
|
| 268 |
+
return (
|
| 269 |
+
" ".join(parts).replace(" <br> ", "<br>").replace("<br> ", "<br>").replace(" <br>", "<br>")
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def _cards_html(state: dict) -> str:
|
| 274 |
+
cards = [c for c in state["cards"] if _norm(c["before"]) != _norm(c["after"])]
|
| 275 |
+
if not cards:
|
| 276 |
+
return ""
|
| 277 |
+
parts = []
|
| 278 |
+
for c in cards:
|
| 279 |
+
color = STAGE_COLORS[c["stage"] % len(STAGE_COLORS)]
|
| 280 |
+
if c["status"] == "์ ์ฉ":
|
| 281 |
+
cls, label = "ok", "โ ์ ์ฉ"
|
| 282 |
+
elif c["status"] == "ํ์ธ ํ์":
|
| 283 |
+
cls, label = "chk", "ํ์ธ ํ์"
|
| 284 |
+
else:
|
| 285 |
+
cls, label = "rej", c["status"]
|
| 286 |
+
ev_html = (
|
| 287 |
+
f"<div class='muted'>๊ทผ๊ฑฐ: {html.escape(c['evidence'])}</div>" if c["evidence"] else ""
|
| 288 |
+
)
|
| 289 |
+
parts.append(
|
| 290 |
+
f"<div class='card' style='border-left:5px solid {color}'><span class='muted'>"
|
| 291 |
+
f"{STAGES[c['stage']][0]}{' ยท ' + c['cat'] if c['cat'] else ''}</span>"
|
| 292 |
+
f"<span class='chip {cls}'>{label}</span><br>"
|
| 293 |
+
f"<s>{html.escape(c['before'])}</s> โ <b>{html.escape(c['after'])}</b>{ev_html}</div>"
|
| 294 |
+
)
|
| 295 |
+
return f"<div class='muted' style='margin-top:6px'>์ ์ {len(cards)}๊ฑด</div>" + "".join(parts)
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def render(ph: Any, state: dict, now: float) -> None:
|
| 299 |
+
"""์ผ์ชฝ: ์์ฝ 3์นธ ยท ๋จ๊ณ ์คํ
ํผ ยท ์ํ ์ค / ์ค๋ฅธ์ชฝ: ํ์ด๋ผ์ดํธ ๋ณธ๋ฌธ ยท ์์ ์นด๋."""
|
| 300 |
+
elapsed = now - state["started"]
|
| 301 |
+
expected = sum(s[2] for s in STAGES)
|
| 302 |
+
with ph.container():
|
| 303 |
+
left, right = st.columns([2, 3], gap="large")
|
| 304 |
+
with left:
|
| 305 |
+
n_ok = sum(c["status"] == "์ ์ฉ" for c in state["cards"])
|
| 306 |
+
n_chk = sum(c["status"] == "ํ์ธ ํ์" for c in state["cards"])
|
| 307 |
+
if state["error"]:
|
| 308 |
+
took = "์คํจ"
|
| 309 |
+
elif state["done"]:
|
| 310 |
+
took = f"{state['done']['elapsed_s']:.1f}์ด"
|
| 311 |
+
else:
|
| 312 |
+
took = f"{elapsed:.0f}์ดโฆ"
|
| 313 |
+
st.markdown(
|
| 314 |
+
"<div class='stats'>"
|
| 315 |
+
f"<div class='stat'><div class='k'>์์</div><div class='v'>{took}</div></div>"
|
| 316 |
+
f"<div class='stat'><div class='k'>์ ์ฉ</div><div class='v'>{n_ok}</div></div>"
|
| 317 |
+
f"<div class='stat'><div class='k'>ํ์ธ ํ์</div><div class='v'>{n_chk}</div></div>"
|
| 318 |
+
"</div>",
|
| 319 |
+
unsafe_allow_html=True,
|
| 320 |
+
)
|
| 321 |
+
lines = []
|
| 322 |
+
for i, (name, _step, typical) in enumerate(STAGES):
|
| 323 |
+
prev = state["stage_done_at"].get(i - 1, 0.0)
|
| 324 |
+
if i in state["stage_done_at"]:
|
| 325 |
+
lines.append(
|
| 326 |
+
f"<div class='stage'>โ {name} <span class='muted'>({state['stage_done_at'][i] - prev:.1f}์ด)</span></div>"
|
| 327 |
+
)
|
| 328 |
+
elif i == state["stage_i"] and not state["done"] and not state["error"]:
|
| 329 |
+
lines.append(
|
| 330 |
+
f"<div class='stage'>โ <b>{name}</b> <span class='muted'>์งํ ์ค {elapsed - prev:.0f}์ด ยท ๋ณดํต {typical:.0f}์ด</span></div>"
|
| 331 |
+
)
|
| 332 |
+
else:
|
| 333 |
+
lines.append(f"<div class='stage muted'>โ {name}</div>")
|
| 334 |
+
st.markdown("".join(lines), unsafe_allow_html=True)
|
| 335 |
+
if state["error"]:
|
| 336 |
+
st.markdown(
|
| 337 |
+
f"<div class='status' style='color:#b00020'>์คํจ: {html.escape(state['error'])}</div>",
|
| 338 |
+
unsafe_allow_html=True,
|
| 339 |
+
)
|
| 340 |
+
elif state["done"]:
|
| 341 |
+
st.markdown(
|
| 342 |
+
f"<div class='status' style='color:#1e7d3a'>์๋ฃ {state['done']['elapsed_s']:.1f}์ด</div>",
|
| 343 |
+
unsafe_allow_html=True,
|
| 344 |
+
)
|
| 345 |
+
else:
|
| 346 |
+
msg = state["log"]
|
| 347 |
+
if elapsed > expected * 1.5 + 5:
|
| 348 |
+
msg += f" ยท ํ์({expected:.0f}์ด)๋ณด๋ค ์ค๋ ๊ฑธ๋ฆฝ๋๋ค - API ๋๊ธฐ ์ค์ผ ์ ์์ต๋๋ค. ๊ฒฐ๊ณผ๋ ๊ทธ๋๋ก ๋์ฐฉํฉ๋๋ค."
|
| 349 |
+
st.markdown(
|
| 350 |
+
f"<div class='status muted'>{elapsed:.0f}์ด ๊ฒฝ๊ณผ ยท {msg}<div class='shimmer'></div></div>",
|
| 351 |
+
unsafe_allow_html=True,
|
| 352 |
+
)
|
| 353 |
+
with right:
|
| 354 |
+
st.markdown(f"<div class='pane'>{_render_text(state)}</div>", unsafe_allow_html=True)
|
| 355 |
+
# ์นด๋๋ ํ ๋ฉ์ด๋ฆฌ HTML ๋ก ๊ทธ๋ฆฐ๋ค - ์นด๋๋ง๋ค st.markdown ์ ๋ถ๋ฅด๋ฉด ์นด๋๊ฐ ์ค์์ ๋ ์ง๋ ๋ ๋์ ์นด๋๊ฐ ๋จ๋๋ค
|
| 356 |
+
st.markdown(_cards_html(state), unsafe_allow_html=True)
|
runner.py
CHANGED
|
@@ -19,8 +19,10 @@ config ์์ฐ(ํ์ดํ๋ผ์ธ yamlยทํ๋กฌํํธยท์นํ ์ฌ์ )์
|
|
| 19 |
from __future__ import annotations
|
| 20 |
|
| 21 |
import asyncio
|
|
|
|
|
|
|
|
|
|
| 22 |
import time
|
| 23 |
-
from collections.abc import Callable
|
| 24 |
from dataclasses import dataclass
|
| 25 |
from pathlib import Path
|
| 26 |
from typing import Any
|
|
@@ -55,9 +57,6 @@ CONFIG = Config(
|
|
| 55 |
#: ํ์ํ ํค. ๊ธฐ์ฌ ๊ฒํ (Opus)๊ฐ Timely ๋ฅผ ๊ฑฐ์น๋ค.
|
| 56 |
REQUIRED_KEYS = ("UPSTAGE_API_KEY", "TIMELY_API_KEY")
|
| 57 |
|
| 58 |
-
# (event, index, total, step_name) - ์คํ
์ด ๋๋ ๋๋ง๋ค "done" ์ผ๋ก ๋ถ๋ฆฐ๋ค
|
| 59 |
-
StepCallback = Callable[[str, int, int, str], None]
|
| 60 |
-
|
| 61 |
|
| 62 |
def find_config_dir() -> Path:
|
| 63 |
"""ํ์ดํ๋ผ์ธ yaml ์ ์ค์ ๋ก ๊ฐ์ง ์์ฐ ๋ฃจํธ. Space ๋ฒ๋ค์ด ๋ ํฌ๋ณด๋ค ๋จผ์ ๋ค."""
|
|
@@ -97,49 +96,76 @@ def build_pipeline() -> Any:
|
|
| 97 |
)
|
| 98 |
|
| 99 |
|
| 100 |
-
def
|
| 101 |
-
"""
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
|
| 105 |
-
async def _run_async(pipeline: Any, text: str,
|
| 106 |
from solar_eval.models.sample import EvalSample
|
| 107 |
from solar_eval.providers import INFERENCE_ROUTES, RoutedProvider
|
| 108 |
from solar_eval.providers.upstage import UpstageProvider
|
| 109 |
|
| 110 |
-
names = step_names(pipeline)
|
| 111 |
-
|
| 112 |
-
def hook(name: str, _sample: Any, _ctx: Any) -> None:
|
| 113 |
-
if on_step is not None:
|
| 114 |
-
on_step("done", names.index(name) + 1, len(names), name)
|
| 115 |
-
|
| 116 |
-
sample = EvalSample(input={"original": text})
|
| 117 |
-
provider = RoutedProvider(UpstageProvider(), {k: c() for k, c in INFERENCE_ROUTES.items()})
|
| 118 |
start = time.monotonic()
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
"""
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
from __future__ import annotations
|
| 20 |
|
| 21 |
import asyncio
|
| 22 |
+
import difflib
|
| 23 |
+
import queue
|
| 24 |
+
import threading
|
| 25 |
import time
|
|
|
|
| 26 |
from dataclasses import dataclass
|
| 27 |
from pathlib import Path
|
| 28 |
from typing import Any
|
|
|
|
| 57 |
#: ํ์ํ ํค. ๊ธฐ์ฌ ๊ฒํ (Opus)๊ฐ Timely ๋ฅผ ๊ฑฐ์น๋ค.
|
| 58 |
REQUIRED_KEYS = ("UPSTAGE_API_KEY", "TIMELY_API_KEY")
|
| 59 |
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
def find_config_dir() -> Path:
|
| 62 |
"""ํ์ดํ๋ผ์ธ yaml ์ ์ค์ ๋ก ๊ฐ์ง ์์ฐ ๋ฃจํธ. Space ๋ฒ๋ค์ด ๋ ํฌ๋ณด๋ค ๋จผ์ ๋ค."""
|
|
|
|
| 96 |
)
|
| 97 |
|
| 98 |
|
| 99 |
+
def word_changes(before: str, after: str) -> list[tuple[str, str]]:
|
| 100 |
+
"""์ด์ diff ๋ก (๋ฐ๋๊ธฐ ์ , ๋ฐ๋ ํ) ์. ๊ท์นยท์ฌ์์ฑ ์คํ
์ ๋ณ๊ฒฝ์ ์นด๋๋ก ๋ง๋ค ๋ ์ด๋ค."""
|
| 101 |
+
a, b = before.split(), after.split()
|
| 102 |
+
sm = difflib.SequenceMatcher(a=a, b=b, autojunk=False)
|
| 103 |
+
return [
|
| 104 |
+
(" ".join(a[i1:i2]), " ".join(b[j1:j2]))
|
| 105 |
+
for op, i1, i2, j1, j2 in sm.get_opcodes()
|
| 106 |
+
if op != "equal"
|
| 107 |
+
]
|
| 108 |
|
| 109 |
|
| 110 |
+
async def _run_async(pipeline: Any, text: str, events: queue.Queue) -> None:
|
| 111 |
from solar_eval.models.sample import EvalSample
|
| 112 |
from solar_eval.providers import INFERENCE_ROUTES, RoutedProvider
|
| 113 |
from solar_eval.providers.upstage import UpstageProvider
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
start = time.monotonic()
|
| 116 |
+
prev = {"text": text}
|
| 117 |
+
|
| 118 |
+
def hook(name: str, sample: Any, ctx: Any) -> None:
|
| 119 |
+
changes = word_changes(prev["text"], ctx.text)
|
| 120 |
+
prev["text"] = ctx.text
|
| 121 |
+
events.put(
|
| 122 |
+
{
|
| 123 |
+
"kind": "step",
|
| 124 |
+
"step": name,
|
| 125 |
+
"text": ctx.text,
|
| 126 |
+
"changes": changes, # ์ด ์คํ
์ด ๋ณธ๋ฌธ์ ์ค์ ๋ก ๋ธ ๋ณ๊ฒฝ(์ด์ ์)
|
| 127 |
+
"corrections": dict(sample.artifacts.get("corrections") or {}),
|
| 128 |
+
"json": dict(
|
| 129 |
+
sample.artifacts.get("json") or {}
|
| 130 |
+
), # ํ์ ์คํ
๊ฒฐ๊ณผ - ๋๋๋ฆผ ๋ฒํธยท์ฌ์
|
| 131 |
+
"hunks": dict(sample.artifacts.get("hunks") or {}), # diff_hunks ๋ณ๊ฒฝ ๋ชฉ๋ก
|
| 132 |
+
"elapsed_s": time.monotonic() - start,
|
| 133 |
+
}
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
provider = RoutedProvider(UpstageProvider(), {k: c() for k, c in INFERENCE_ROUTES.items()})
|
| 138 |
+
# ํ๊ฐ run ๊ณผ ๊ฐ์ ์กฐ๊ฑด: temp 0.0, reasoning off
|
| 139 |
+
result = await pipeline.run(
|
| 140 |
+
EvalSample(input={"original": text}),
|
| 141 |
+
prompts="",
|
| 142 |
+
provider=provider,
|
| 143 |
+
model=CONFIG.model,
|
| 144 |
+
temperature=0.0,
|
| 145 |
+
max_tokens=8000,
|
| 146 |
+
reasoning_effort=None,
|
| 147 |
+
on_step=hook,
|
| 148 |
+
)
|
| 149 |
+
except Exception as e: # noqa: BLE001 - ๋ฐ๋ชจ ํ๋ฉด: ์์ธ๋ง ํ๋ฉด์ผ๋ก
|
| 150 |
+
events.put(
|
| 151 |
+
{
|
| 152 |
+
"kind": "error",
|
| 153 |
+
"error": f"{type(e).__name__}: {e}",
|
| 154 |
+
"elapsed_s": time.monotonic() - start,
|
| 155 |
+
}
|
| 156 |
+
)
|
| 157 |
+
return
|
| 158 |
+
events.put({"kind": "done", "output": result.output, "elapsed_s": time.monotonic() - start})
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def start_proofread(pipeline: Any, text: str) -> queue.Queue:
|
| 162 |
+
"""ํ
์คํธ 1๊ฑด ๊ต์ด์ ์ค๋ ๋๋ก ์์ํ๊ณ ์ด๋ฒคํธ ํ๋ฅผ ๋๋ ค์ค๋ค.
|
| 163 |
+
|
| 164 |
+
์ด๋ฒคํธ: {"kind": "step", step, text, changes, corrections, json, hunks, elapsed_s} ๋ฅผ ์คํ
๋ง๋ค,
|
| 165 |
+
๋๋๋ฉด {"kind": "done", output, elapsed_s} ๋๋ {"kind": "error", error, elapsed_s} ํ๋.
|
| 166 |
"""
|
| 167 |
+
events: queue.Queue = queue.Queue()
|
| 168 |
+
threading.Thread(
|
| 169 |
+
target=lambda: asyncio.run(_run_async(pipeline, text, events)), daemon=True
|
| 170 |
+
).start()
|
| 171 |
+
return events
|