"""
Text preprocessing and script analysis module.
Preserves native Indic scripts and cleans text.
"""
from __future__ import annotations
import html
import re
import unicodedata
from typing import Any, Dict, List, Optional
import torch
# The 22 scheduled Indic languages. This is separate from the training dataset's
# supported_languages setting, which describes the languages in its annotations.
SCHEDULED_INDIC_LANGUAGES = (
"Assamese",
"Bengali",
"Bodo",
"Dogri",
"Gujarati",
"Hindi",
"Kannada",
"Kashmiri",
"Konkani",
"Maithili",
"Malayalam",
"Manipuri",
"Marathi",
"Nepali",
"Odia",
"Punjabi",
"Sanskrit",
"Santali",
"Sindhi",
"Tamil",
"Telugu",
"Urdu",
)
# Unicode blocks map to a representative language when multiple languages
# share a script. Assamese has distinct letters in the Bengali block.
INDIC_UNICODE_RANGES = {
"Hindi": (0x0900, 0x097F),
"Punjabi": (0x0A00, 0x0A7F),
"Gujarati": (0x0A80, 0x0AFF),
"Odia": (0x0B00, 0x0B7F),
"Tamil": (0x0B80, 0x0BFF),
"Telugu": (0x0C00, 0x0C7F),
"Kannada": (0x0C80, 0x0CFF),
"Malayalam": (0x0D00, 0x0D7F),
"Bengali": (0x0980, 0x09FF),
"Urdu": (0x0600, 0x06FF),
"Manipuri": (0xABC0, 0xABFF),
"Santali": (0x1C50, 0x1C7F),
}
ASSAMESE_UNIQUE_CHARACTERS = frozenset("\u09f0\u09f1")
def clean_indic_text(text: Any) -> str:
"""
Clean and normalize Indic text while strictly preserving native scripts.
Operations:
1. Converts input to string; handles None or NaN.
2. Unescapes HTML entities (&, <, >, etc.).
3. Removes HTML tags (
,
, etc.). 4. Normalizes Unicode representations to NFC format. 5. Normalizes multiple whitespace characters, tabs, and newlines into a single space. 6. Strips leading and trailing whitespace. IMPORTANT: Native characters, punctuation, and numerals are never deleted. """ if text is None: return "" if not isinstance(text, str): text = str(text) # 1. Unescape HTML entities text = html.unescape(text) # 2. Strip HTML tags if present text = re.sub(r"<[^>]+>", " ", text) # 3. Unicode NFC normalization (ensures consistent diacritic / matra composition) text = unicodedata.normalize("NFC", text) # 4. Collapse whitespace text = re.sub(r"[\s\t\r\n]+", " ", text) # 5. Trim ends return text.strip() def detect_indic_script(text: str) -> Optional[str]: """ Detect a language from the predominant Indic script in the input text. Script detection cannot distinguish languages that share a script (for example Hindi and Marathi). In such cases this returns a representative language; callers should allow the user to specify the language. """ if not text: return None counts = {lang: 0 for lang in INDIC_UNICODE_RANGES} has_assamese_character = False for char in text: code = ord(char) if char in ASSAMESE_UNIQUE_CHARACTERS: has_assamese_character = True for lang, (start, end) in INDIC_UNICODE_RANGES.items(): if start <= code <= end: counts[lang] += 1 break best_lang, best_count = max(counts.items(), key=lambda item: item[1]) if best_count > 0: if best_lang == "Bengali" and has_assamese_character: return "Assamese" return best_lang return None def get_text_statistics(texts: List[str]) -> Dict[str, Any]: """ Compute length statistics (word count, character count) across a collection of texts. """ if not texts: return { "total_texts": 0, "char_len_mean": 0.0, "char_len_min": 0, "char_len_max": 0, "word_len_mean": 0.0, "word_len_min": 0, "word_len_max": 0, } char_lens = [len(t) for t in texts] word_lens = [len(t.split()) for t in texts] return { "total_texts": len(texts), "char_len_mean": round(float(sum(char_lens) / len(char_lens)), 2), "char_len_min": int(min(char_lens)), "char_len_max": int(max(char_lens)), "word_len_mean": round(float(sum(word_lens) / len(word_lens)), 2), "word_len_min": int(min(word_lens)), "word_len_max": int(max(word_lens)), } def tokenize_texts( texts: List[str], tokenizer: Any, max_length: int = 128, return_tensors: Optional[str] = "pt", ) -> Dict[str, torch.Tensor]: """ Tokenize a batch of texts using the given tokenizer with truncation and padding. """ return tokenizer( texts, padding=True, truncation=True, max_length=max_length, return_tensors=return_tensors, )