Spaces:
Sleeping
Sleeping
| """ | |
| app/classifiers/text_preprocessor.py | |
| ====================================== | |
| Unicode-aware text preprocessor that runs before any classifier. | |
| Responsibilities: | |
| 1. Strip invisible / control Unicode characters and flag their presence | |
| (zero-width spaces, RTL overrides, soft hyphens, etc.) | |
| 2. Apply NFKC Unicode normalization to collapse compatibility variants and | |
| homoglyph lookalikes (e.g. Cyrillic 'а' → Latin 'a') | |
| 3. Return cleaned text + metadata dict for downstream bias adjustments | |
| The preprocessor is a pure-Python, ML-free module with no heavy dependencies. | |
| It is called before every classifier (not just hf2) so that even the fast | |
| sklearn path benefits from Unicode normalization. | |
| Metadata flags drive the invisible-char probability bias in input_guard.py: | |
| - had_invisible_chars: True if zero-width or other invisible chars were found | |
| - had_rtl_override: True if a bidirectional control override was found | |
| - had_homoglyphs: True if characters that NFKC normalized significantly differ | |
| - unicode_normalized: True if NFKC changed the text at all | |
| """ | |
| from __future__ import annotations | |
| import unicodedata | |
| class TextPreprocessor: | |
| """ | |
| Stateless text preprocessor for adversarial Unicode normalization. | |
| Usage: | |
| preprocessor = TextPreprocessor() | |
| cleaned, meta = preprocessor.preprocess(raw_text) | |
| # Use cleaned for classifier, meta for bias decisions | |
| """ | |
| # Characters that are invisible / zero-width in most rendering contexts | |
| # and have no semantic value in legitimate user input. | |
| INVISIBLE_CHARS: frozenset[str] = frozenset([ | |
| '\u200B', # zero-width space | |
| '\u200C', # zero-width non-joiner | |
| '\u200D', # zero-width joiner | |
| '\u2060', # word joiner | |
| '\u00AD', # soft hyphen | |
| '\uFEFF', # BOM / zero-width no-break space | |
| '\u200E', # left-to-right mark | |
| '\u200F', # right-to-left mark | |
| '\u202A', # LTR embedding | |
| '\u202B', # RTL embedding | |
| '\u202C', # pop directional formatting | |
| '\u202D', # LTR override | |
| '\u202E', # RTL override (most dangerous — reverses displayed text) | |
| '\u2066', # LTR isolate | |
| '\u2067', # RTL isolate | |
| '\u2068', # first strong isolate | |
| '\u2069', # pop directional isolate | |
| ]) | |
| # RTL override chars are a strong signal on their own | |
| RTL_OVERRIDE_CHARS: frozenset[str] = frozenset([ | |
| '\u202E', # RTL override | |
| '\u202B', # RTL embedding | |
| '\u2067', # RTL isolate | |
| ]) | |
| def preprocess(self, text: str) -> tuple[str, dict[str, bool]]: | |
| """ | |
| Normalize *text* for safe classifier input. | |
| Returns: | |
| (cleaned_text, metadata) | |
| metadata keys: | |
| had_invisible_chars: bool — invisible/zero-width chars were found | |
| had_rtl_override: bool — RTL control chars were found (strong signal) | |
| unicode_normalized: bool — NFKC changed the text (covers homoglyphs) | |
| """ | |
| if not text: | |
| return text, { | |
| "had_invisible_chars": False, | |
| "had_rtl_override": False, | |
| "unicode_normalized": False, | |
| } | |
| had_invisible = False | |
| had_rtl = False | |
| # ── Step 1: detect and strip invisible/control chars ────────────────── | |
| stripped_chars: list[str] = [] | |
| for ch in text: | |
| if ch in self.INVISIBLE_CHARS: | |
| had_invisible = True | |
| if ch in self.RTL_OVERRIDE_CHARS: | |
| had_rtl = True | |
| # Strip — do not add to output | |
| continue | |
| stripped_chars.append(ch) | |
| stripped = "".join(stripped_chars) | |
| # ── Step 2: NFKC normalization ───────────────────────────────────────── | |
| # NFKC collapses compatibility variants and maps many Unicode lookalikes | |
| # (Cyrillic, Greek, fullwidth, superscript digits) to their ASCII | |
| # equivalents. This handles the 'homoglyph' attack class automatically. | |
| normalized = unicodedata.normalize("NFKC", stripped) | |
| unicode_normalized = normalized != text | |
| return normalized, { | |
| "had_invisible_chars": had_invisible, | |
| "had_rtl_override": had_rtl, | |
| "unicode_normalized": unicode_normalized, | |
| } | |