Spaces:
Sleeping
Sleeping
Download preprocessing.py from nhdang117/ToxicScope: direct link, hf CLI and curl.
- Browser
- Download file 8.33 kB
-
https://huggingface.co/spaces/nhdang117/ToxicScope/resolve/main/preprocessing.py
- Command line
-
hf download hf://spaces/nhdang117/ToxicScope/preprocessing.py
-
curl -L -o preprocessing.py https://huggingface.co/spaces/nhdang117/ToxicScope/resolve/main/preprocessing.py
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() | |
| 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) | |
| 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 | |
| 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'} | |
| 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)) | |
| 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>' | |