Spaces:
Runtime error
Runtime error
Delete preprocessor.py
Browse files- preprocessor.py +0 -268
preprocessor.py
DELETED
|
@@ -1,268 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Advanced Text Preprocessing for Multilingual Indic Text
|
| 3 |
-
Handles Unicode normalization, noise removal, script detection preprocessing.
|
| 4 |
-
"""
|
| 5 |
-
from __future__ import annotations
|
| 6 |
-
|
| 7 |
-
import re
|
| 8 |
-
import unicodedata
|
| 9 |
-
from dataclasses import dataclass
|
| 10 |
-
from typing import Dict, List, Optional, Tuple
|
| 11 |
-
|
| 12 |
-
import numpy as np
|
| 13 |
-
from loguru import logger
|
| 14 |
-
|
| 15 |
-
from src.utils.languages import SCRIPT_UNICODE_BLOCKS, LANGUAGE_REGISTRY
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
# ─────────────────────────────────────────────
|
| 19 |
-
# Unicode Script Detection
|
| 20 |
-
# ─────────────────────────────────────────────
|
| 21 |
-
|
| 22 |
-
def get_char_script(char: str) -> str:
|
| 23 |
-
"""Get Unicode script name for a single character."""
|
| 24 |
-
try:
|
| 25 |
-
cat = unicodedata.category(char)
|
| 26 |
-
name = unicodedata.name(char, "")
|
| 27 |
-
# Extract script from Unicode name (e.g. "DEVANAGARI LETTER A" → "Devanagari")
|
| 28 |
-
parts = name.split()
|
| 29 |
-
if not parts:
|
| 30 |
-
return "Unknown"
|
| 31 |
-
first = parts[0].capitalize()
|
| 32 |
-
# Multi-word scripts
|
| 33 |
-
for script in SCRIPT_UNICODE_BLOCKS:
|
| 34 |
-
if script.upper() in name.upper():
|
| 35 |
-
return script
|
| 36 |
-
return first
|
| 37 |
-
except (ValueError, TypeError):
|
| 38 |
-
return "Unknown"
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def get_dominant_script(text: str) -> Tuple[str, Dict[str, float]]:
|
| 42 |
-
"""
|
| 43 |
-
Detect the dominant script in text.
|
| 44 |
-
Returns (dominant_script, script_distribution).
|
| 45 |
-
"""
|
| 46 |
-
script_counts: Dict[str, int] = {}
|
| 47 |
-
total_alpha = 0
|
| 48 |
-
|
| 49 |
-
for char in text:
|
| 50 |
-
if not char.isalpha():
|
| 51 |
-
continue
|
| 52 |
-
total_alpha += 1
|
| 53 |
-
cp = ord(char)
|
| 54 |
-
|
| 55 |
-
matched = False
|
| 56 |
-
for script, ranges in SCRIPT_UNICODE_BLOCKS.items():
|
| 57 |
-
for lo, hi in ranges:
|
| 58 |
-
if lo <= cp <= hi:
|
| 59 |
-
script_counts[script] = script_counts.get(script, 0) + 1
|
| 60 |
-
matched = True
|
| 61 |
-
break
|
| 62 |
-
if matched:
|
| 63 |
-
break
|
| 64 |
-
if not matched:
|
| 65 |
-
script_counts["Other"] = script_counts.get("Other", 0) + 1
|
| 66 |
-
|
| 67 |
-
if total_alpha == 0:
|
| 68 |
-
return "Unknown", {}
|
| 69 |
-
|
| 70 |
-
dist = {s: c / total_alpha for s, c in script_counts.items()}
|
| 71 |
-
dominant = max(dist, key=dist.get) if dist else "Unknown"
|
| 72 |
-
return dominant, dist
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
# ─────────────────────────────────────────────
|
| 76 |
-
# Preprocessor Config
|
| 77 |
-
# ─────────────────────────────────────────────
|
| 78 |
-
|
| 79 |
-
@dataclass
|
| 80 |
-
class PreprocessorConfig:
|
| 81 |
-
normalize_unicode: bool = True
|
| 82 |
-
nfc_form: str = "NFC"
|
| 83 |
-
remove_urls: bool = True
|
| 84 |
-
remove_emails: bool = True
|
| 85 |
-
remove_html_tags: bool = True
|
| 86 |
-
normalize_whitespace: bool = True
|
| 87 |
-
remove_zero_width: bool = True
|
| 88 |
-
handle_mixed_numerals: bool = True
|
| 89 |
-
preserve_script_punctuation: bool = True
|
| 90 |
-
lowercase_latin: bool = False
|
| 91 |
-
max_length: Optional[int] = 2000
|
| 92 |
-
min_length: int = 10
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
# ─────────────────────────────────────────────
|
| 96 |
-
# Main Preprocessor
|
| 97 |
-
# ─────────────────────────────────────────────
|
| 98 |
-
|
| 99 |
-
class IndicTextPreprocessor:
|
| 100 |
-
"""
|
| 101 |
-
Comprehensive text preprocessor for Indic multilingual corpora.
|
| 102 |
-
Handles Unicode normalization, noise removal, and script-aware cleaning.
|
| 103 |
-
"""
|
| 104 |
-
|
| 105 |
-
# Regex patterns
|
| 106 |
-
_URL_RE = re.compile(
|
| 107 |
-
r"https?://\S+|www\.\S+|ftp://\S+", re.UNICODE
|
| 108 |
-
)
|
| 109 |
-
_EMAIL_RE = re.compile(
|
| 110 |
-
r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
|
| 111 |
-
)
|
| 112 |
-
_HTML_RE = re.compile(r"<[^>]+>")
|
| 113 |
-
_WHITESPACE_RE = re.compile(r"\s+", re.UNICODE)
|
| 114 |
-
_ZERO_WIDTH_RE = re.compile(
|
| 115 |
-
r"[\u200b\u200c\u200d\u200e\u200f\u202a-\u202e\ufeff\u00ad]"
|
| 116 |
-
)
|
| 117 |
-
_MULTI_PUNCT_RE = re.compile(r"([!?।॥,;.]){2,}")
|
| 118 |
-
|
| 119 |
-
# Indic numeral mappings to ASCII (for normalization)
|
| 120 |
-
_NUMERAL_MAP = str.maketrans(
|
| 121 |
-
"".join([
|
| 122 |
-
"०१२३४५६७८९", # Devanagari
|
| 123 |
-
"০১২৩৪৫৬৭৮৯", # Bengali
|
| 124 |
-
"੦੧੨੩੪੫੬੭੮੯", # Gurmukhi
|
| 125 |
-
"૦૧૨૩૪૫૬૭૮૯", # Gujarati
|
| 126 |
-
"୦୧୨୩୪୫୬୭୮୯", # Odia
|
| 127 |
-
"௦௧௨௩௪௫௬௭௮௯", # Tamil
|
| 128 |
-
"౦౧౨౩౪౫౬౭౮౯", # Telugu
|
| 129 |
-
"೦೧೨೩೪೫೬೭೮೯", # Kannada
|
| 130 |
-
"൦൧൨൩൪൫൬൭൮൯", # Malayalam
|
| 131 |
-
]),
|
| 132 |
-
"0123456789" * 9
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
def __init__(self, config: Optional[PreprocessorConfig] = None):
|
| 136 |
-
self.config = config or PreprocessorConfig()
|
| 137 |
-
|
| 138 |
-
def preprocess(self, text: str) -> str:
|
| 139 |
-
"""Full preprocessing pipeline."""
|
| 140 |
-
if not text or not isinstance(text, str):
|
| 141 |
-
return ""
|
| 142 |
-
|
| 143 |
-
cfg = self.config
|
| 144 |
-
|
| 145 |
-
# 1. Unicode normalization
|
| 146 |
-
if cfg.normalize_unicode:
|
| 147 |
-
text = unicodedata.normalize(cfg.nfc_form, text)
|
| 148 |
-
|
| 149 |
-
# 2. Remove zero-width and invisible characters
|
| 150 |
-
if cfg.remove_zero_width:
|
| 151 |
-
text = self._ZERO_WIDTH_RE.sub("", text)
|
| 152 |
-
|
| 153 |
-
# 3. Remove HTML tags
|
| 154 |
-
if cfg.remove_html_tags:
|
| 155 |
-
text = self._HTML_RE.sub(" ", text)
|
| 156 |
-
|
| 157 |
-
# 4. Remove URLs
|
| 158 |
-
if cfg.remove_urls:
|
| 159 |
-
text = self._URL_RE.sub(" ", text)
|
| 160 |
-
|
| 161 |
-
# 5. Remove emails
|
| 162 |
-
if cfg.remove_emails:
|
| 163 |
-
text = self._EMAIL_RE.sub(" ", text)
|
| 164 |
-
|
| 165 |
-
# 6. Numeral normalization (optional)
|
| 166 |
-
if cfg.handle_mixed_numerals:
|
| 167 |
-
text = text.translate(self._NUMERAL_MAP)
|
| 168 |
-
|
| 169 |
-
# 7. Collapse multiple punctuation
|
| 170 |
-
text = self._MULTI_PUNCT_RE.sub(r"\1", text)
|
| 171 |
-
|
| 172 |
-
# 8. Normalize whitespace
|
| 173 |
-
if cfg.normalize_whitespace:
|
| 174 |
-
text = self._WHITESPACE_RE.sub(" ", text).strip()
|
| 175 |
-
|
| 176 |
-
# 9. Lowercase Latin (optional, e.g. for English)
|
| 177 |
-
if cfg.lowercase_latin:
|
| 178 |
-
text = self._selective_lowercase(text)
|
| 179 |
-
|
| 180 |
-
# 10. Length truncation
|
| 181 |
-
if cfg.max_length and len(text) > cfg.max_length:
|
| 182 |
-
text = text[:cfg.max_length]
|
| 183 |
-
|
| 184 |
-
return text
|
| 185 |
-
|
| 186 |
-
def _selective_lowercase(self, text: str) -> str:
|
| 187 |
-
"""Lowercase only Latin characters, preserve others."""
|
| 188 |
-
result = []
|
| 189 |
-
for ch in text:
|
| 190 |
-
cp = ord(ch)
|
| 191 |
-
# Latin Basic + Extended
|
| 192 |
-
if 0x0041 <= cp <= 0x005A or 0x00C0 <= cp <= 0x00D6:
|
| 193 |
-
result.append(ch.lower())
|
| 194 |
-
else:
|
| 195 |
-
result.append(ch)
|
| 196 |
-
return "".join(result)
|
| 197 |
-
|
| 198 |
-
def batch_preprocess(
|
| 199 |
-
self, texts: List[str], show_progress: bool = True
|
| 200 |
-
) -> List[str]:
|
| 201 |
-
"""Preprocess a batch of texts."""
|
| 202 |
-
if show_progress:
|
| 203 |
-
from tqdm.auto import tqdm
|
| 204 |
-
return [self.preprocess(t) for t in tqdm(texts, desc="Preprocessing")]
|
| 205 |
-
return [self.preprocess(t) for t in texts]
|
| 206 |
-
|
| 207 |
-
def quality_score(self, text: str) -> float:
|
| 208 |
-
"""
|
| 209 |
-
Returns a text quality score in [0, 1].
|
| 210 |
-
Considers: alpha ratio, script consistency, length adequacy.
|
| 211 |
-
"""
|
| 212 |
-
if not text:
|
| 213 |
-
return 0.0
|
| 214 |
-
|
| 215 |
-
alpha = sum(c.isalpha() for c in text)
|
| 216 |
-
total = len(text)
|
| 217 |
-
if total == 0:
|
| 218 |
-
return 0.0
|
| 219 |
-
|
| 220 |
-
alpha_ratio = alpha / total
|
| 221 |
-
|
| 222 |
-
_, script_dist = get_dominant_script(text)
|
| 223 |
-
script_concentration = max(script_dist.values()) if script_dist else 0.0
|
| 224 |
-
|
| 225 |
-
length_score = min(1.0, len(text) / 100)
|
| 226 |
-
|
| 227 |
-
return 0.3 * alpha_ratio + 0.4 * script_concentration + 0.3 * length_score
|
| 228 |
-
|
| 229 |
-
def compute_text_features(self, text: str) -> Dict:
|
| 230 |
-
"""Rich feature set for a single text (used in analysis)."""
|
| 231 |
-
dominant, dist = get_dominant_script(text)
|
| 232 |
-
n_chars = len(text)
|
| 233 |
-
n_alpha = sum(c.isalpha() for c in text)
|
| 234 |
-
n_digits = sum(c.isdigit() for c in text)
|
| 235 |
-
n_punct = sum(unicodedata.category(c).startswith("P") for c in text)
|
| 236 |
-
n_spaces = sum(c.isspace() for c in text)
|
| 237 |
-
unique_chars = len(set(text))
|
| 238 |
-
type_token_ratio = unique_chars / max(n_chars, 1)
|
| 239 |
-
|
| 240 |
-
# Character entropy
|
| 241 |
-
from collections import Counter as _Counter
|
| 242 |
-
char_counts = _Counter(text)
|
| 243 |
-
total_f = sum(char_counts.values())
|
| 244 |
-
char_entropy = -sum(
|
| 245 |
-
(c / total_f) * np.log2(c / total_f)
|
| 246 |
-
for c in char_counts.values() if c > 0
|
| 247 |
-
) if total_f > 0 else 0.0
|
| 248 |
-
|
| 249 |
-
return {
|
| 250 |
-
"n_chars": n_chars,
|
| 251 |
-
"n_alpha": n_alpha,
|
| 252 |
-
"n_digits": n_digits,
|
| 253 |
-
"n_punct": n_punct,
|
| 254 |
-
"n_spaces": n_spaces,
|
| 255 |
-
"alpha_ratio": n_alpha / max(n_chars, 1),
|
| 256 |
-
"digit_ratio": n_digits / max(n_chars, 1),
|
| 257 |
-
"unique_char_ratio": type_token_ratio,
|
| 258 |
-
"dominant_script": dominant,
|
| 259 |
-
"script_distribution": dist,
|
| 260 |
-
"script_count": len(dist),
|
| 261 |
-
"is_mixed_script": len(dist) > 1 and max(dist.values()) < 0.95,
|
| 262 |
-
"quality_score": self.quality_score(text),
|
| 263 |
-
"n_words": len(text.split()),
|
| 264 |
-
"avg_word_len": (
|
| 265 |
-
np.mean([len(w) for w in text.split()]) if text.split() else 0
|
| 266 |
-
),
|
| 267 |
-
"char_entropy": float(char_entropy),
|
| 268 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|