Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -46,3 +46,199 @@ def apply_name_tags(text: str, names: list, start_index: int = 100) -> tuple[str
|
|
| 46 |
mapping[tag] = name
|
| 47 |
counter += 1
|
| 48 |
return tagged_text, mapping
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
mapping[tag] = name
|
| 47 |
counter += 1
|
| 48 |
return tagged_text, mapping
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# 📦 PART 2: 호칭/조사 확장기 + 태그 매핑 보정기
|
| 53 |
+
|
| 54 |
+
import re
|
| 55 |
+
|
| 56 |
+
# 호칭 + 조사 리스트
|
| 57 |
+
COMMON_SUFFIXES = [
|
| 58 |
+
# 📁 가정/관계 기반
|
| 59 |
+
'어머니', '아버지', '엄마', '아빠', '형', '누나', '언니', '오빠', '동생',
|
| 60 |
+
'딸', '아들', '조카', '사촌', '이모', '고모', '삼촌', '숙모', '외삼촌',
|
| 61 |
+
'할머니', '할아버지', '외할머니', '외할아버지', '장모', '장인', '며느리', '사위',
|
| 62 |
+
'부인', '와이프', '신랑', '올케', '형수', '제수씨', '매형', '처제', '시누이',
|
| 63 |
+
|
| 64 |
+
# 📁 사회/교육/직업 호칭
|
| 65 |
+
'학생', '초등학생', '중학생', '고등학생', '수험생', '학부모', '선생', '선생님', '교사',
|
| 66 |
+
'교감', '교장', '담임', '반장', '조교수', '교수', '연구원', '강사', '박사', '석사', '학사',
|
| 67 |
+
'보호자', '피해자', '아동', '주민', '당사자', '대상자', '담당자',
|
| 68 |
+
|
| 69 |
+
# 📁 직장/조직 직급
|
| 70 |
+
'대표', '이사', '전무', '상무', '부장', '차장', '과장', '대리', '사원', '팀장', '본부장',
|
| 71 |
+
'센터장', '소장', '실장', '총무', '직원', '매니저', '지점장', '사무장',
|
| 72 |
+
|
| 73 |
+
# 📁 의료/기타
|
| 74 |
+
'의사', '간호사', '간병인', '기사님', '어르신', '님', '씨'
|
| 75 |
+
]
|
| 76 |
+
|
| 77 |
+
COMMON_JOSA = [
|
| 78 |
+
# 🧱 기본 주격/목적격/보격 조사
|
| 79 |
+
'이', '가', '을', '를', '은', '는', '의', '도',
|
| 80 |
+
|
| 81 |
+
# 📍 처소/이유/방향
|
| 82 |
+
'에', '에서', '에게', '으로', '로', '부터', '까지', '한테',
|
| 83 |
+
|
| 84 |
+
# 🌀 강조/비교/대조
|
| 85 |
+
'보다', '마저', '조차', '까지도', '조차도', '밖에', '만큼', '만큼은', '이라도', '이든지', '이나마',
|
| 86 |
+
|
| 87 |
+
# 🎯 연결/강조/기타
|
| 88 |
+
'이며', '이나', '이거나', '이라서', '이니까', '이라면', '이지만', '처럼', '대로', '하고', '그리고',
|
| 89 |
+
|
| 90 |
+
# 🧩 보조표현형 어미
|
| 91 |
+
'이기도', '이었던', '이었지만', '이어서', '이었다면', '인', '일', '임', '이란', '이라는',
|
| 92 |
+
|
| 93 |
+
# 📦 특수형 조사 (복합 + 종결형)
|
| 94 |
+
'같은', '같아서', '까지는', '뿐만 아니라', '와는', '와도', '하고도', '으로서', '으로써'
|
| 95 |
+
]
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def expand_variation_patterns(text: str, mapping: dict) -> str:
|
| 99 |
+
"""
|
| 100 |
+
👓 태그된 텍스트에서 성+이름+호칭+조사 형태를 다시 태깅
|
| 101 |
+
예: '고은비학생이' → 'N100이'
|
| 102 |
+
"""
|
| 103 |
+
for tag, base in mapping.items():
|
| 104 |
+
prefix = r'[\s\(\["'‘“]*' # 시작 구두점/공백 등
|
| 105 |
+
suffix = f"(?:{'|'.join(COMMON_SUFFIXES)})?"
|
| 106 |
+
josa = f"(?:{'|'.join(COMMON_JOSA)})?"
|
| 107 |
+
pattern = re.compile(rf'{prefix}{re.escape(base)}{suffix}{josa}', re.IGNORECASE)
|
| 108 |
+
text = pattern.sub(lambda m: m.group(0).replace(base, tag), text)
|
| 109 |
+
return text
|
| 110 |
+
|
| 111 |
+
def boost_mapping_from_context(text: str, mapping: dict) -> dict:
|
| 112 |
+
"""
|
| 113 |
+
📌 태깅된 텍스트에서 각 태그의 실제 확장된 표현 감지해 mapping 보정
|
| 114 |
+
예: '고은비학생이' → 'N100', 매핑: N100 → '고은비학생이'
|
| 115 |
+
"""
|
| 116 |
+
updated = {}
|
| 117 |
+
for tag, base in mapping.items():
|
| 118 |
+
idx = text.find(tag)
|
| 119 |
+
if idx == -1:
|
| 120 |
+
updated[tag] = base
|
| 121 |
+
continue
|
| 122 |
+
window = text[max(0, idx - 100): idx + 100]
|
| 123 |
+
pattern = re.compile(rf'([가-힣]+)?{re.escape(base)}(?:{"|".join(COMMON_SUFFIXES)})?(?:{"|".join(COMMON_JOSA)})?')
|
| 124 |
+
match = pattern.search(window)
|
| 125 |
+
if match:
|
| 126 |
+
updated[tag] = match.group(0)
|
| 127 |
+
else:
|
| 128 |
+
updated[tag] = base
|
| 129 |
+
return updated
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# 📦 PART 3: 민감정보 마스커 + 학교/학년/학과 마스커
|
| 134 |
+
|
| 135 |
+
import re
|
| 136 |
+
|
| 137 |
+
def postprocess_sensitive_patterns(text: str) -> str:
|
| 138 |
+
"""
|
| 139 |
+
🔐 이메일, 주민등록번호, 계좌번호, 카드번호, 전화번호, 주소 마스킹
|
| 140 |
+
"""
|
| 141 |
+
text = re.sub(r"[\w\.-]+@[\w\.-]+", "******@***.***", text) # 이메일
|
| 142 |
+
text = re.sub(r"(\d{6})[- ]?(\d{7})", "******-*******", text) # 주민번호
|
| 143 |
+
text = re.sub(r"(\d{3})[- ]?(\d{4})[- ]?(\d{4})", "***-****-****", text) # 카드/전화
|
| 144 |
+
text = re.sub(r"(\d{1,3})동", "***동", text)
|
| 145 |
+
text = re.sub(r"(\d{1,4})호", "****호", text)
|
| 146 |
+
return text
|
| 147 |
+
|
| 148 |
+
def to_chosung(text: str) -> str:
|
| 149 |
+
"""
|
| 150 |
+
🧠 초성 변환기: 학교명, 학과명 등에 적용
|
| 151 |
+
"""
|
| 152 |
+
CHOSUNG_LIST = [chr(i) for i in range(0x1100, 0x1113)]
|
| 153 |
+
result = ""
|
| 154 |
+
for ch in text:
|
| 155 |
+
if '가' <= ch <= '힣':
|
| 156 |
+
code = ord(ch) - ord('가')
|
| 157 |
+
cho = code // 588
|
| 158 |
+
result += CHOSUNG_LIST[cho]
|
| 159 |
+
else:
|
| 160 |
+
result += ch
|
| 161 |
+
return result
|
| 162 |
+
|
| 163 |
+
def mask_school_names(text: str) -> str:
|
| 164 |
+
"""
|
| 165 |
+
🏫 학교명 → 초성 변환 마스킹 (연세대학교 → ㅇㅅ대학교)
|
| 166 |
+
"""
|
| 167 |
+
def replace_school(m):
|
| 168 |
+
return to_chosung(m.group(1)) + m.group(2)
|
| 169 |
+
return re.sub(r"([가-힣]{2,20})(초등학교|중학교|고등학교|대학교)", replace_school, text)
|
| 170 |
+
|
| 171 |
+
def mask_department_names(text: str) -> str:
|
| 172 |
+
"""
|
| 173 |
+
🏢 학과명 → 초성 마스킹 (국문학과 → ㄱㅁ학과)
|
| 174 |
+
"""
|
| 175 |
+
return re.sub(r"([가-힣]{2,20})학과", lambda m: to_chosung(m.group(1)) + "학과", text)
|
| 176 |
+
|
| 177 |
+
def mask_grade_class(text: str) -> str:
|
| 178 |
+
"""
|
| 179 |
+
🎓 학년/반 정보 마스킹 (2학년 3반 → *학년 *반)
|
| 180 |
+
"""
|
| 181 |
+
return re.sub(r"(\d)학년(\s?(\d)반)?", "*학년 *반", text)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
# 📦 PART 4: 기관 키워드 치환기 + Gradio UI 실행기
|
| 186 |
+
|
| 187 |
+
import re
|
| 188 |
+
import gradio as gr
|
| 189 |
+
from part1_name_extract_and_tag import extract_names, apply_name_tags
|
| 190 |
+
from part2_suffix_expansion_and_mapping import expand_variation_patterns, boost_mapping_from_context
|
| 191 |
+
from part3_sensitive_school_masker import (
|
| 192 |
+
postprocess_sensitive_patterns,
|
| 193 |
+
mask_school_names,
|
| 194 |
+
mask_department_names,
|
| 195 |
+
mask_grade_class
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
def replace_institution_keywords(text: str, keywords: list, replace_word: str) -> str:
|
| 199 |
+
"""
|
| 200 |
+
🏢 키워드 기반 기관명 → 치환어로 변경
|
| 201 |
+
"""
|
| 202 |
+
for kw in keywords:
|
| 203 |
+
pattern = re.compile(rf'([\s\(\["'‘“]*){re.escape(kw)}([가-힣\s.,;:!?()"'”’]*)', re.IGNORECASE)
|
| 204 |
+
text = pattern.sub(lambda m: m.group(1) + replace_word + m.group(2), text)
|
| 205 |
+
return text
|
| 206 |
+
|
| 207 |
+
def apply_full_masking(text: str, keyword_str: str, replace_word: str):
|
| 208 |
+
# 1. 키워드 치환
|
| 209 |
+
keywords = [k.strip() for k in keyword_str.split(",") if k.strip()]
|
| 210 |
+
text = replace_institution_keywords(text, keywords, replace_word)
|
| 211 |
+
|
| 212 |
+
# 2. 민감정보 + 학교 학과 학년 마스킹
|
| 213 |
+
text = postprocess_sensitive_patterns(text)
|
| 214 |
+
text = mask_school_names(text)
|
| 215 |
+
text = mask_department_names(text)
|
| 216 |
+
text = mask_grade_class(text)
|
| 217 |
+
|
| 218 |
+
# 3. 이름 추출 + 태깅
|
| 219 |
+
names = extract_names(text)
|
| 220 |
+
tagged, mapping = apply_name_tags(text, names)
|
| 221 |
+
|
| 222 |
+
# 4. 파생 표현 확장
|
| 223 |
+
tagged = expand_variation_patterns(tagged, mapping)
|
| 224 |
+
mapping = boost_mapping_from_context(tagged, mapping)
|
| 225 |
+
|
| 226 |
+
# 5. 매핑 출력 정리
|
| 227 |
+
mapping_text = "\n".join([f"{k} → {v}" for k, v in mapping.items()])
|
| 228 |
+
return tagged, mapping_text
|
| 229 |
+
|
| 230 |
+
# UI 실행
|
| 231 |
+
with gr.Blocks() as demo:
|
| 232 |
+
gr.Markdown("🧠 **v5.0 마스킹 통합 시스템** — 키워드, 이름, 개인정보, 학교 마스킹")
|
| 233 |
+
input_text = gr.Textbox(lines=15, label="📄 원문 텍스트")
|
| 234 |
+
keyword_input = gr.Textbox(lines=1, label="기관 키워드 (쉼표로 구분)", value="굿네이버스, 사회복지법인 굿네이버스")
|
| 235 |
+
replace_input = gr.Textbox(lines=1, label="치환할 텍스트", value="우리기관")
|
| 236 |
+
run_button = gr.Button("🚀 실행")
|
| 237 |
+
masked_output = gr.Textbox(lines=15, label="🔐 마스킹 결과")
|
| 238 |
+
mapping_output = gr.Textbox(lines=10, label="🏷️ 태그 매핑", interactive=False)
|
| 239 |
+
run_button.click(fn=apply_full_masking, inputs=[input_text, keyword_input, replace_input], outputs=[masked_output, mapping_output])
|
| 240 |
+
|
| 241 |
+
demo.launch()
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
|