| """ |
| span_postproc.py β Q3 κ²½κ³μ€λ₯ CHEAP νμ²λ¦¬ (μ¬νμ΅ 0). litreview 2026-06-04 Β§3 + q3-boundary-fix-plan. |
| CHEAP-1: νκΈ(ζΌ’ε) μ£Όμ / μ§ μ λ§λ κ΄νΈ 꼬리λ₯Ό PS/OG/LC κ³ μ λͺ
μμ νΈλ¦¬λ°. |
| νκ΅μ΄ νΌμ©νκΈ° 'νκΈ(ζΌ’ε)' κ΄λ‘μ νμ μ£Όμμ mention μ λ―Έν¬ν¨ β 'μ¬μ(ιθ₯Ώ' β 'μ¬μ'. |
| μμ νμ κ³ μ λͺ
(νκΈ prefix μμ)μ 보쑴 β νκΈ prefix κ° μμ λλ§ νμ/κ΄νΈ 꼬리 μ κ±°. |
| CHEAP-3: type-aware stoplist (gold FP μλ, 보μμ ). μΌλ°μ΄/μ§μμ΄λ§, gold μ λ΅κ³Ό 무좩λμΈ μλ§. |
| λ¬Έλ§₯ μμ‘΄ μ΄ν(μ λΆ=μ€μ κΈ°κ΄μΌ μλ, ν 건=gold κ° QT λ‘ μν¨)λ *μ μΈ* β κ³Όμ΅μ λ°©μ§. |
| μ μ© μμΉ: λ°νμ/νκ° μΆμΆ *λ§μ§λ§* (idiom_filter λ€μ, canon+model+regex+idiom λ³ν© ν). |
| NER λͺ¨λΈ/regex/canon/silver λ μ 건λλ¦Ό β μμ μΆλ ₯ νμ²λ¦¬. |
| 곡μ : νμ΄μ¬ νκ°κ²½λ‘(eval_full_pipelineΒ·gold_eval) + μλ²(hf-space/app.py) + (ν¬ν
μμ ) plugin ner.js. |
| app.py λ offset(start/end) λλ°μ΄λΌ trim_span_offsets λ‘ surface+offset λμ νΈλ¦¬λ°. |
| |
| self-test: νμ¬ gold-multibot FP μΌμ΄μ€κ° μλλλ‘ νΈλ¦¬λ°/μ΅μ λλμ§. |
| μ€ν: py span_postproc.py |
| from span_postproc import postprocess, trim_affix, nfc |
| """ |
| import sys |
| import unicodedata |
|
|
| try: |
| sys.stdout.reconfigure(encoding="utf-8") |
| except Exception: |
| pass |
|
|
| |
| _OPEN = "οΌ(οΌ»[γγο½’γ{οΌ<" |
| _CLOSE = "οΌ)οΌ½]γγο½£γ}οΌ>" |
|
|
|
|
| def _is_hangul(c): |
| return "κ°" <= c <= "ν£" |
|
|
|
|
| def has_hangul(s): |
| """λ¬Έμμ΄μ νκΈ μμ μ΄ νλλΌλ μλ β νμ-only/κ΄νΈ μμ¬(μ: pipeline μ΄ μͺΌκ° 'ιθ₯Ώ') νλ³μ©.""" |
| return any(_is_hangul(c) for c in (s or "")) |
|
|
|
|
| def _is_cjk(c): |
| """CJK ν΅ν©νμ(+νμ₯ A/νΈν). νκΈ prefix λ€ νμ μ£Όμ νμ μ©.""" |
| o = ord(c) |
| return (0x3400 <= o <= 0x9FFF) or (0xF900 <= o <= 0xFAFF) or (0x20000 <= o <= 0x2FA1F) |
|
|
|
|
| def trim_affix(surface): |
| """PS/OG/LC surface μ νμ μ£Όμ/κ΄νΈ 꼬리 μ κ±°. νκΈ prefix κ° μμ λλ§ μλ¦. |
| |
| 'μ¬μ(ιθ₯Ώ' β 'μ¬μ' Β· 'μ¬μ(ιθ₯Ώ)' β 'μ¬μ' Β· 'νκ°(ζΌ’ζ±)' β 'νκ°' |
| 'μ€λ ¨ν' β 'μ€λ ¨ν' (λ³ν μμ) Β· 'ιθ₯Ώ' β 'ιθ₯Ώ' (μμ νμ 보쑴) |
| """ |
| s = (surface or "").strip() |
| if not s: |
| return s |
| |
| if any(_is_hangul(c) for c in s): |
| for i, c in enumerate(s): |
| if (c in _OPEN or _is_cjk(c)) and any(_is_hangul(p) for p in s[:i]): |
| s = s[:i] |
| break |
| |
| s = s.strip().strip(_OPEN + _CLOSE).strip() |
| return s |
|
|
|
|
| def trim_span_offsets(text, start, end): |
| """offset λλ° νΈλ¦¬λ° (app.py / plugin parity). (start, end, surface) λ°ν. |
| |
| surface = text[start:end] λ₯Ό trim_affix ν λ€, μλ €λκ° λ§νΌ end λ₯Ό λΉκ²¨ offset μΌκ΄ μ μ§. |
| μμͺ½μ΄ μ리λ κ²½μ°(λ«λ κ΄νΈλ‘ μμ λ±)λ start λ μ μ§. |
| """ |
| surf = text[start:end] |
| trimmed = trim_affix(surf) |
| if trimmed == surf: |
| return start, end, surf |
| |
| off = surf.find(trimmed) if trimmed else 0 |
| new_start = start + off |
| new_end = new_start + len(trimmed) |
| return new_start, new_end, trimmed |
|
|
|
|
| |
| |
| STOP_PAIRS = { |
| ("κ·Έλ ", "DT"), |
| ("μλ°©", "PS"), |
| ("νλ", "PS"), |
| ("λ λ¬Έν", "QT"), |
| ("μ΄λμ μ", "PS"), |
| } |
|
|
|
|
| def is_stopword(surface, typ): |
| return (surface, typ) in STOP_PAIRS |
|
|
|
|
| |
| _TRIM_TYPES = ("PS", "OG", "LC") |
|
|
|
|
| def postprocess(preds): |
| """[(surface, type), ...] β CHEAP-1 νΈλ¦¬λ° + CHEAP-3 stoplist μ μ©. idiom_filter λ€μ μ리. |
| |
| νΈλ¦¬λ°μΌλ‘ λΉ λ¬Έμμ΄μ΄ λλ©΄ λλ‘. νΈλ¦¬λ°μ΄ λ§λ (surface,type) μ€λ³΅λ μ κ±°. |
| """ |
| out = [] |
| seen = set() |
| for p in preds: |
| surf, typ = p[0], p[1] |
| if typ in _TRIM_TYPES: |
| s = trim_affix(surf) |
| if not has_hangul(s): |
| continue |
| else: |
| s = (surf or "").strip() |
| if not s: |
| continue |
| if is_stopword(s, typ): |
| continue |
| if (s, typ) in seen: |
| continue |
| seen.add((s, typ)) |
| out.append((s, typ)) |
| return out |
|
|
|
|
| def nfc(text): |
| """CHEAP-4: μ
λ ₯/gold λ₯Ό NFC λ‘ ν΅μΌ (μ κ°Β·κ²°ν©λ¬Έμ offset μ΄κΈλ¨ λ°©μ§).""" |
| return unicodedata.normalize("NFC", text or "") |
|
|
|
|
| if __name__ == "__main__": |
| trim_cases = [ |
| ("μ¬μ(ιθ₯Ώ", "μ¬μ"), |
| ("μ¬μ(ιθ₯Ώ)", "μ¬μ"), |
| ("μ¬μ(", "μ¬μ"), |
| ("νκ°(ζΌ’ζ±)", "νκ°"), |
| ("μλ¨ ν΄", "μλ¨ ν΄"), |
| ("μ€λ ¨ν", "μ€λ ¨ν"), |
| ("ιθ₯Ώ", "ιθ₯Ώ"), |
| ("μ€λ¦¬μν
μ΄μ
λ£Έ", "μ€λ¦¬μν
μ΄μ
λ£Έ"), |
| ] |
| ok = 0 |
| print("[trim] CHEAP-1 νμ/κ΄νΈ 꼬리 νΈλ¦¬λ°:") |
| for src, exp in trim_cases: |
| got = trim_affix(src) |
| flag = "OK" if got == exp else "XX" |
| ok += got == exp |
| print(f" [{flag}] trim_affix({src!r}) = {got!r} (κΈ°λ {exp!r})") |
|
|
| stop_cases = [ |
| (("νλ", "PS"), True), |
| (("νλ", "QT"), False), |
| (("κ·Έλ ", "DT"), True), |
| (("μλ°©", "PS"), True), |
| (("λ λ¬Έν", "QT"), True), |
| (("μ΄λμ μ", "PS"), True), |
| (("μ λΆ", "OG"), False), |
| (("ν 건", "QT"), False), |
| ] |
| print("\n[stop] CHEAP-3 type-aware stoplist:") |
| for (surf, typ), exp in stop_cases: |
| got = is_stopword(surf, typ) |
| flag = "OK" if got == exp else "XX" |
| ok += got == exp |
| print(f" [{flag}] is_stopword({surf!r}, {typ!r}) = {got} (κΈ°λ {exp})") |
|
|
| |
| print("\n[post] ν΅ν© νμ²λ¦¬:") |
| pp = postprocess([("μ¬μ(", "LC"), ("ιθ₯Ώ", "LC"), ("νλ", "PS"), ("κ·Έλ ", "DT"), ("μλ¨ ν΄", "PS")]) |
| exp_pp = [("μ¬μ", "LC"), ("μλ¨ ν΄", "PS")] |
| flag = "OK" if pp == exp_pp else "XX" |
| ok += pp == exp_pp |
| print(f" [{flag}] postprocess(...) = {pp} (κΈ°λ {exp_pp})") |
|
|
| s0, e0, surf0 = trim_span_offsets("μ¬μ(ιθ₯Ώ)μ 거리λ", 0, 5) |
| flag = "OK" if surf0 == "μ¬μ" and (s0, e0) == (0, 2) else "XX" |
| ok += surf0 == "μ¬μ" and (s0, e0) == (0, 2) |
| print(f" [{flag}] trim_span_offsets = ({s0},{e0},{surf0!r}) (κΈ°λ (0,2,'μ¬μ'))") |
|
|
| total = len(trim_cases) + len(stop_cases) + 2 |
| print(f"\n[span_postproc] self-test {ok}/{total} ν΅κ³Ό") |
|
|