ToxicScope / preprocessing.py
nhdang117's picture
Super-squash branch 'main' using huggingface_hub
2d741b1
Raw History Blame Contribute Delete
8.33 kB
"""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>'