Download python/bhc/text.py from sourojitdotcom/AksharaLP-Sentence-2.8M: direct link, hf CLI and curl.
- Browser
- Download file 4.61 kB
-
https://huggingface.co/sourojitdotcom/AksharaLP-Sentence-2.8M/resolve/main/python/bhc/text.py
- Command line
-
hf download hf://sourojitdotcom/AksharaLP-Sentence-2.8M/python/bhc/text.py
-
curl -L -o text.py https://huggingface.co/sourojitdotcom/AksharaLP-Sentence-2.8M/resolve/main/python/bhc/text.py
4.61 kB
| """Text normalisation, Unicode validation and tokenisation shared by data prep, baselines and eval.""" | |
| import re | |
| import unicodedata | |
| ZW = "\u200c\u200d\u200b\ufeff\u00ad" # ZWNJ, ZWJ, ZWSP, BOM, soft hyphen | |
| _ZW_RE = re.compile(f"[{ZW}]") | |
| _WS_RE = re.compile(r"\s+") | |
| # Devanagari character classes | |
| CONS = set(chr(c) for c in range(0x0915, 0x093A)) | set(chr(c) for c in range(0x0958, 0x0960)) | set("ॹॺॻॼॽॾॿ") | |
| INDEP_V = set(chr(c) for c in range(0x0904, 0x0915)) | set("ॠॡॲॳॴॵॶॷ") | |
| MATRA = set(chr(c) for c in range(0x093E, 0x094D)) | set("ॢॣॏॕॖॗ\u093a\u093b\u094e") | |
| VIRAMA = "\u094d" | |
| NUKTA = "\u093c" | |
| NASAL = set("\u0901\u0902") # chandrabindu, anusvara | |
| VISARGA = "\u0903" | |
| DEVA_DIGITS = "०१२३४५६७८९" | |
| DEVA_RANGE = re.compile(r"[\u0900-\u097F]") | |
| DANDA = "।॥" | |
| def nfc(s: str) -> str: | |
| return unicodedata.normalize("NFC", s) | |
| def is_malformed(s: str) -> bool: | |
| """True if the string contains characters we treat as malformed/garbage: | |
| replacement char, private use, surrogates, unassigned, or control chars other than tab/newline.""" | |
| for ch in s: | |
| if ch == "\ufffd": | |
| return True | |
| cat = unicodedata.category(ch) | |
| if cat in ("Co", "Cs", "Cn"): | |
| return True | |
| if cat == "Cc" and ch not in "\t\n": | |
| return True | |
| return False | |
| def clean(s: str) -> str: | |
| """NFC + strip zero-width/format chars + collapse whitespace.""" | |
| s = nfc(s) | |
| s = _ZW_RE.sub("", s) | |
| s = "".join(ch for ch in s if unicodedata.category(ch) != "Cf") | |
| return _WS_RE.sub(" ", s).strip() | |
| def valid_devanagari_word(w: str) -> bool: | |
| """Orthographic sanity check for a single Devanagari word (letters only). | |
| Rejects: non-Devanagari chars, leading combining marks, matra/virama/nukta after a non-consonant, | |
| two dependent vowel signs in a row, nukta not on a consonant, dangling virama+matra.""" | |
| if not w: | |
| return False | |
| prev = None | |
| for i, ch in enumerate(w): | |
| if not ("\u0900" <= ch <= "\u097F"): | |
| return False | |
| if ch in DEVA_DIGITS or ch in DANDA: | |
| return False | |
| if ch in MATRA or ch == VIRAMA or ch == NUKTA: | |
| if prev is None: | |
| return False | |
| if ch == NUKTA and prev not in CONS: | |
| return False | |
| if ch in MATRA and not (prev in CONS or prev == NUKTA): | |
| return False | |
| if ch == VIRAMA and not (prev in CONS or prev == NUKTA): | |
| return False | |
| if ch in NASAL or ch == VISARGA: | |
| if prev is None or prev in NASAL or prev == VIRAMA: | |
| return False | |
| prev = ch | |
| return True | |
| def valid_roman_word(w: str) -> bool: | |
| return bool(re.fullmatch(r"[a-z]+", w)) | |
| # --- tokenisation ----------------------------------------------------------- | |
| # Words (Latin or Devanagari letters incl. marks), numbers, URLs/emails/handles, punctuation. | |
| TOKEN_RE = re.compile( | |
| r"(https?://\S+|www\.\S+|[\w.+-]+@[\w-]+\.[\w.]+|[@#][\w_]+" | |
| r"|[A-Za-z]+(?:'[A-Za-z]+)?" | |
| r"|[\u0900-\u0963\u0971-\u097F]+" | |
| r"|[0-9\u0966-\u096F]+(?:[.,:][0-9\u0966-\u096F]+)*" | |
| r"|[\u0964\u0965]|[^\sA-Za-z0-9\u0900-\u097F])" | |
| ) | |
| def tokenize(s: str): | |
| return TOKEN_RE.findall(s) | |
| PUNCT_RE = re.compile(r"[^\w\u0900-\u097F]+", re.UNICODE) | |
| # --- scoring normalisation -------------------------------------------------- | |
| _DIGIT_TR = str.maketrans(DEVA_DIGITS, "0123456789") | |
| def norm_for_score(s: str) -> str: | |
| """'norm' scoring view: NFC, no zero-width chars, Devanagari digits -> ASCII, punctuation removed | |
| (incl. danda), whitespace collapsed. Spelling is otherwise untouched (strict on letters).""" | |
| s = clean(s).translate(_DIGIT_TR) | |
| s = re.sub(r"[^\w\s\u0900-\u097F]|[\u0964\u0965]", " ", s) | |
| return _WS_RE.sub(" ", s).strip() | |
| _FOLD_MAP = { | |
| "\u0901": "\u0902", # chandrabindu -> anusvara (हूँ ~ हूं) | |
| NUKTA: "", # drop nukta (ज़ ~ ज) | |
| "\u0945": "\u0947", # candra e (ॅ) -> e | |
| "\u0949": "\u094b", # candra o (ॉ) -> o (ऑफिस ~ ओफिस) | |
| "\u0911": "\u0913", # independent candra O -> O | |
| "\u090d": "\u090f", | |
| } | |
| def fold(s: str) -> str: | |
| """'fold' scoring view (variant-tolerant, reported separately): norm_for_score + nukta dropped, | |
| chandrabindu->anusvara, candra vowels -> plain. Never used as the primary metric.""" | |
| s = unicodedata.normalize("NFD", norm_for_score(s)) # decompose precomposed nukta letters | |
| s = "".join(_FOLD_MAP.get(ch, ch) for ch in s) | |
| return unicodedata.normalize("NFC", s) | |