File size: 7,734 Bytes
d787d0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b718f01
 
 
 
 
d787d0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b718f01
 
 
 
 
 
d787d0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b718f01
 
d787d0f
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
"""
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

# ── CHEAP-1: κ΄„ν˜Έ/ν•œμž 꼬리 트리밍 ──
_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
    # ν•œκΈ€ prefix + (μ—¬λŠ”κ΄„ν˜Έ | ν•œμžλΈ”λ‘) μ‹œμž‘ μ§€μ μ—μ„œ μ»· (κ·Έ μ•žμ— ν•œκΈ€μ΄ μ‹€μž¬ν•  λ•Œλ§Œ)
    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
    # trimmed λŠ” 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


# ── CHEAP-3: type-aware stoplist (보수적) ──
# 각 μŒμ€ ν˜„μž¬ gold-multibot FP 에 μ‹€μž¬ + gold μ •λ‹΅κ³Ό 무좩돌. λ¬Έλ§₯ μ˜μ‘΄μ–΄λŠ” μ˜λ„μ μœΌλ‘œ μ œμ™Έ.
STOP_PAIRS = {
    ("κ·Έλ‚ ", "DT"),       # μ§€μ‹œ ν‘œν˜„ (괴이탐사 'κ·Έλ‚  저녁')
    ("μ„œλ°©", "PS"),       # 호칭 'μ„œλ°©λ‹˜'
    ("ν•˜λ‚˜", "PS"),       # μˆ˜μ‚¬ '단 ν•˜λ‚˜' (gold λŠ” ν•˜λ‚˜=QT 둜 원함 β†’ PS 만 차단)
    ("두 문파", "QT"),    # 일반 μˆ˜λŸ‰+λͺ…사 ('ν™”μ‚°νŒŒμ™€ μ’…λ‚¨νŒŒ = 두 문파')
    ("μ΄λŒ€μ œμž", "PS"),   # λ¬΄ν˜‘ 제자 λ“±κΈ‰ (일반 λͺ…사, 인물 μ•„λ‹˜)
}


def is_stopword(surface, typ):
    return (surface, typ) in STOP_PAIRS


# ── 톡합 ν›„μ²˜λ¦¬ ──
_TRIM_TYPES = ("PS", "OG", "LC")  # ν•œμž 주석 κ΄€λ‘€λŠ” 고유λͺ…μ—λ§Œ (DT/TI/QT λŠ” κ΄„ν˜Έ μ•ˆ μˆ«μžκ°€ μ •λ‹Ή)


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):       # 트리밍 ν›„ ν•œκΈ€ 0 = ν•œμž/κ΄„ν˜Έ μž”μž¬(pipeline 이 μͺΌκ°  '陝θ₯Ώ' λ“±) β†’ λ“œλ‘­
                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 = [
        ("μ„¬μ„œ(陝θ₯Ώ", "μ„¬μ„œ"),
        ("μ„¬μ„œ(陝θ₯Ώ)", "μ„¬μ„œ"),
        ("μ„¬μ„œ(", "μ„¬μ„œ"),
        ("ν•œκ°•(漒江)", "ν•œκ°•"),
        ("에단 ν‚΄", "에단 ν‚΄"),     # λ³€ν™” μ—†μŒ
        ("μ˜€λ ¨ν‘œ", "μ˜€λ ¨ν‘œ"),       # λ³€ν™” μ—†μŒ
        ("陝θ₯Ώ", "陝θ₯Ώ"),           # 순수 ν•œμž 보쑴 (ν•œκΈ€ prefix μ—†μŒ)
        ("μ˜€λ¦¬μ—”ν…Œμ΄μ…˜ λ£Έ", "μ˜€λ¦¬μ—”ν…Œμ΄μ…˜ λ£Έ"),
    ]
    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),    # gold μ •λ‹΅ β€” 보쑴
        (("κ·Έλ‚ ", "DT"), True),
        (("μ„œλ°©", "PS"), True),
        (("두 문파", "QT"), True),
        (("μ΄λŒ€μ œμž", "PS"), True),
        (("μ •λΆ€", "OG"), False),    # gold μ •λ‹΅ β€” 보쑴
        (("ν•œ 건", "QT"), False),   # gold μ •λ‹΅ β€” 보쑴
    ]
    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})")

    # 톡합: μ„¬μ„œ( κ°€ νŠΈλ¦¬λ°λ˜μ–΄ gold 와 λ§€μΉ­λ˜λŠ”μ§€ + offset 트리밍
    print("\n[post] 톡합 ν›„μ²˜λ¦¬:")
    pp = postprocess([("μ„¬μ„œ(", "LC"), ("陝θ₯Ώ", "LC"), ("ν•˜λ‚˜", "PS"), ("κ·Έλ‚ ", "DT"), ("에단 ν‚΄", "PS")])
    exp_pp = [("μ„¬μ„œ", "LC"), ("에단 ν‚΄", "PS")]   # 陝θ₯Ώ(ν•œμžonly)Β·ν•˜λ‚˜(PS)Β·κ·Έλ‚ (DT) λ“œλ‘­
    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)  # 'μ„¬μ„œ(陝θ₯Ώ' span
    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} 톡과")