Spaces:
Sleeping
Sleeping
File size: 8,326 Bytes
2d741b1 | 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 184 185 186 187 188 189 190 191 | """Training-aligned PyVi and RDRSegmenter pipelines, with exact text caches."""
import atexit, gzip, html, json, re, threading, unicodedata
from functools import lru_cache
from pathlib import Path
from runtime_setup import activate_java
import text_pipeline as tp
ROOT = Path(__file__).resolve().parent
_segmenter = None
_segment_lock = threading.Lock()
@lru_cache(maxsize=1)
def cached_corpus():
path = ROOT / 'resources/preprocessed_cache.json.gz'
if not path.exists():
return {}
with gzip.open(path, 'rt', encoding='utf-8') as f:
return json.load(f)
@lru_cache(maxsize=1)
def stopwords():
words = set()
for line in (ROOT / 'resources/vietnamese-stopwords.txt').read_text(encoding='utf-8').splitlines():
w = line.strip().lower()
if w:
words.update([w, w.replace(' ', '_')])
return words
@lru_cache(maxsize=25000)
def basic(text):
raw = str(text).strip()
hit = cached_corpus().get(raw)
if hit:
return dict(clean=hit[0], seg_text=hit[1], nostop_text=hit[2],
inference_text=normalize_raw(raw), obfuscation_changed=False,
obfuscation_replacements=[], robust_mode='training_cache',
source='training_pipeline_cache')
if not tp.HAS_EMOJI_LIB or not tp.HAS_TONE_LIB:
raise RuntimeError('Thiếu emoji/viet-text-tools. Chạy lại 01_CAI_DAT.bat để khớp tiền xử lý lúc train.')
inference_text, replacements = normalize_obfuscation(raw)
clean, tokens, seg = tp.to_seg_text(inference_text)
nostop = ' '.join(w for w in tokens if w.lower() not in stopwords())
return dict(clean=clean, seg_text=seg, nostop_text=nostop,
inference_text=inference_text,
obfuscation_changed=bool(replacements),
obfuscation_replacements=replacements,
robust_mode='training_lexicon_leetspeak_v1',
source='live_pyvi_robust' if replacements else 'live_pyvi')
def normalize_raw(text):
text = unicodedata.normalize('NFC', html.unescape(str(text)))
text = re.sub('[\u200b\u200c\u200d\ufeff]', '', text)
return re.sub(r'\s+', ' ', text).strip()
# Chỉ giải mã leetspeak khi từ sau giải mã đã xuất hiện trong chính pipeline
# Train/Dev/Test. Cách này nhận được c0n -> con, h3o -> heo nhưng tránh biến
# các mã hợp lệ như H1N1, F0 hoặc COVID-19 thành từ tùy ý.
LEET_TOKEN_RE = re.compile(r'(?<!\w)[\w]*\d[\w]*(?!\w)', re.UNICODE)
LEET_BLOCK_RE = re.compile(r'^(?:f\d{1,2}|h\d+n\d+|[a-z]\d{2,})$', re.IGNORECASE)
BARE_URL_RE = re.compile(r'\b(?:[a-z0-9-]+\.)+(?:com|net|org|vn|io|co|edu|gov)(?:/\S*)?', re.IGNORECASE)
OC_FOLLOW_RE = re.compile(r'^[\s._-]+(?:h3o|heo|ch0|chó|cho|cứt|cut|vật|lợn|lon)\b', re.IGNORECASE)
LEET_CHARS = {'0': 'o', '1': 'i', '3': 'e', '4': 'a',
'5': 's', '7': 't', '8': 'b', '9': 'g'}
LEET_EXACT = {'0c': 'óc', 'm4y': 'mày', 't4o': 'tao'}
@lru_cache(maxsize=1)
def training_lexicon():
"""Từ vựng cho phép dùng khi giải mã, lấy từ dữ liệu đã xử lý thật.
Không dùng nhãn và không thêm tín hiệu TOXIC. Từ vựng chỉ quyết định một
token chữ-số có phải là cách viết né tránh hợp lý hay không.
"""
words = set(stopwords())
for values in cached_corpus().values():
for value in values[:3]:
for token in str(value).casefold().split():
words.add(token)
words.update(part for part in token.split('_') if part)
for key, value in tp.TEENCODE_MAP.items():
words.add(key.casefold())
words.update(value.casefold().replace('_', ' ').split())
words.update(LEET_EXACT.values())
return words
def _overlaps(span, protected):
start, end = span
return any(start < right and end > left for left, right in protected)
def normalize_obfuscation(text):
"""Giải mã leetspeak có kiểm soát và trả cả nhật ký thay thế.
URL, email và số điện thoại được bảo vệ. Mọi thay thế đều có thể kiểm tra
trong JSON/CSV và giao diện; hàm này không gán nhãn nội dung.
"""
normalized = normalize_raw(text)
protected = []
for pattern in (tp.URL_RE, BARE_URL_RE, tp.EMAIL_RE, tp.PHONE_RE):
protected.extend(match.span() for match in pattern.finditer(normalized))
lexicon = training_lexicon()
replacements = []
def replace(match):
original = match.group(0)
start, _ = match.span()
if (_overlaps(match.span(), protected) or
(start and normalized[start-1] == '%') or
not any(ch.isalpha() for ch in original)):
return original
low = original.casefold()
if low in LEET_EXACT:
if low == '0c' and not OC_FOLLOW_RE.match(normalized[match.end():]):
return original
candidate = LEET_EXACT[low]
strategy = 'exact_diacritic_restore'
else:
if LEET_BLOCK_RE.fullmatch(low):
return original
# Kiểu né tránh tổng quát phải có số nằm giữa chữ: c0n, h3o,
# d4ng... Loại số lượng/mốc giờ/mã sản phẩm như 1k, 8h, A4, L3.
if len(original) < 3 or not original[0].isalpha() or not original[-1].isalpha():
return original
if any(ch.isdigit() and ch not in LEET_CHARS for ch in original):
return original
candidate = ''.join(LEET_CHARS.get(ch, ch) for ch in low)
if candidate == low or candidate not in lexicon:
return original
strategy = 'training_lexicon_leetspeak'
if original.isupper():
candidate = candidate.upper()
elif original[:1].isupper():
candidate = candidate[:1].upper() + candidate[1:]
replacements.append(dict(original=original, normalized=candidate,
strategy=strategy))
return candidate
return LEET_TOKEN_RE.sub(replace, normalized), replacements
RISK_PATTERNS = (
('animal_brain_insult', re.compile(r'\b(?:óc|oc)[ _]+(?:chó|cho|heo|lợn|lon|cứt|cut|vật)\b', re.IGNORECASE)),
('direct_personal_insult', re.compile(r'\b(?:đồ|thằng|con|mày|may)[ _]+(?:ngu|đần|khốn|điên|chó|heo|lợn)\b', re.IGNORECASE)),
('restored_profanity', re.compile(r'\b(?:địt|đéo|lồn|cặc)\b', re.IGNORECASE)),
)
def guardrail_assessment(processed):
"""Cờ rà soát độc lập, không tham gia vote và không sửa điểm mô hình."""
searchable = str(processed.get('clean', '')).replace('_', ' ')
matches = []
for category, pattern in RISK_PATTERNS:
for match in pattern.finditer(searchable):
value = match.group(0)
if not any(item['text'].casefold() == value.casefold() for item in matches):
matches.append(dict(category=category, text=value))
changed = bool(processed.get('obfuscation_changed'))
review = changed and bool(matches)
return dict(status='review' if review else ('normalized' if changed else 'clear'),
review_required=review, contributes_to_vote=False, matches=matches,
note=('Phát hiện cách viết né tránh và cụm từ có nguy cơ công kích; '
'cần đọc cùng dự đoán mô hình.' if review else None))
@lru_cache(maxsize=25000)
def phobert_text(text):
global _segmenter
hit = cached_corpus().get(text.strip())
if hit:
return hit[3]
normalized, _ = normalize_obfuscation(text)
if not normalized:
return '<unk>'
with _segment_lock:
if _segmenter is None:
activate_java()
jar = ROOT / 'resources/vncorenlp/VnCoreNLP-1.2.jar'
if not jar.is_file():
raise RuntimeError('Thiếu VnCoreNLP. Chạy python scripts/setup_resources.py.')
from vncorenlp import VnCoreNLP
_segmenter = VnCoreNLP(str(jar), annotators='wseg', max_heap_size='-Xmx512m')
atexit.register(_segmenter.close)
sentences = _segmenter.tokenize(normalized)
return ' '.join(' '.join(s) for s in sentences).strip() or '<unk>'
|