HarshithaaaaReddddyyy commited on
Commit
05db9a6
·
verified ·
1 Parent(s): b945c6a

Delete script_analyzer.py

Browse files
Files changed (1) hide show
  1. script_analyzer.py +0 -462
script_analyzer.py DELETED
@@ -1,462 +0,0 @@
1
- """
2
- Advanced Script Analysis Engine
3
- Deep Unicode-based analysis: script distribution, mixing, directionality,
4
- transliteration detection, noise analysis, and linguistic features.
5
- """
6
- from __future__ import annotations
7
-
8
- import re
9
- import unicodedata
10
- from collections import Counter, defaultdict
11
- from dataclasses import dataclass, field
12
- from typing import Dict, List, Optional, Set, Tuple
13
-
14
- import numpy as np
15
- from loguru import logger
16
-
17
- from src.utils.languages import (
18
- SCRIPT_UNICODE_BLOCKS,
19
- SCRIPT_TO_DIRECTION,
20
- LANGUAGE_REGISTRY,
21
- )
22
-
23
-
24
- # ─────────────────────────────────────────────
25
- # Unicode Block Database
26
- # ─────────────────────────────────────────────
27
-
28
- UNICODE_BLOCK_NAMES: List[Tuple[int, int, str]] = [
29
- (0x0000, 0x007F, "Basic Latin"),
30
- (0x0080, 0x00FF, "Latin-1 Supplement"),
31
- (0x0100, 0x017F, "Latin Extended-A"),
32
- (0x0180, 0x024F, "Latin Extended-B"),
33
- (0x0250, 0x02AF, "IPA Extensions"),
34
- (0x0300, 0x036F, "Combining Diacritical Marks"),
35
- (0x0370, 0x03FF, "Greek"),
36
- (0x0400, 0x04FF, "Cyrillic"),
37
- (0x0500, 0x052F, "Cyrillic Supplement"),
38
- (0x0600, 0x06FF, "Arabic"),
39
- (0x0700, 0x074F, "Syriac"),
40
- (0x0750, 0x077F, "Arabic Supplement"),
41
- (0x0900, 0x097F, "Devanagari"),
42
- (0x0980, 0x09FF, "Bengali"),
43
- (0x0A00, 0x0A7F, "Gurmukhi"),
44
- (0x0A80, 0x0AFF, "Gujarati"),
45
- (0x0B00, 0x0B7F, "Oriya"),
46
- (0x0B80, 0x0BFF, "Tamil"),
47
- (0x0C00, 0x0C7F, "Telugu"),
48
- (0x0C80, 0x0CFF, "Kannada"),
49
- (0x0D00, 0x0D7F, "Malayalam"),
50
- (0x0D80, 0x0DFF, "Sinhala"),
51
- (0x0E00, 0x0E7F, "Thai"),
52
- (0x0F00, 0x0FFF, "Tibetan"),
53
- (0x1000, 0x109F, "Myanmar"),
54
- (0x10A0, 0x10FF, "Georgian"),
55
- (0x1100, 0x11FF, "Hangul Jamo"),
56
- (0x1E00, 0x1EFF, "Latin Extended Additional"),
57
- (0x2000, 0x206F, "General Punctuation"),
58
- (0x20A0, 0x20CF, "Currency Symbols"),
59
- (0x2100, 0x214F, "Letterlike Symbols"),
60
- (0x2200, 0x22FF, "Mathematical Operators"),
61
- (0x3000, 0x303F, "CJK Symbols and Punctuation"),
62
- (0x3040, 0x309F, "Hiragana"),
63
- (0x30A0, 0x30FF, "Katakana"),
64
- (0x4E00, 0x9FFF, "CJK Unified Ideographs"),
65
- (0xFB00, 0xFB4F, "Alphabetic Presentation Forms"),
66
- (0xFB50, 0xFDFF, "Arabic Presentation Forms-A"),
67
- (0xFE70, 0xFEFF, "Arabic Presentation Forms-B"),
68
- (0xFF00, 0xFFEF, "Halfwidth and Fullwidth Forms"),
69
- ]
70
-
71
-
72
- def get_unicode_block(cp: int) -> str:
73
- for lo, hi, name in UNICODE_BLOCK_NAMES:
74
- if lo <= cp <= hi:
75
- return name
76
- return f"U+{cp:04X} (unknown block)"
77
-
78
-
79
- # ─────────────────────────────────────────────
80
- # Data Structures
81
- # ─────────────────────────────────────────────
82
-
83
- @dataclass
84
- class CharacterAnalysis:
85
- char: str
86
- codepoint: int
87
- unicode_name: str
88
- unicode_category: str
89
- unicode_block: str
90
- script: Optional[str]
91
- is_alpha: bool
92
- is_digit: bool
93
- is_punct: bool
94
- is_combining: bool
95
-
96
-
97
- @dataclass
98
- class ScriptSegment:
99
- script: str
100
- start: int
101
- end: int
102
- text: str
103
- char_count: int
104
-
105
-
106
- @dataclass
107
- class ScriptAnalysisResult:
108
- text: str
109
- total_chars: int
110
- alpha_chars: int
111
- digit_chars: int
112
- punct_chars: int
113
- space_chars: int
114
- other_chars: int
115
-
116
- # Script distribution
117
- script_distribution: Dict[str, float]
118
- dominant_script: str
119
- dominant_script_ratio: float
120
-
121
- # Mixing analysis
122
- is_mixed_script: bool
123
- mixing_score: float # 0=pure, 1=maximally mixed
124
- script_segments: List[ScriptSegment]
125
- script_transitions: int
126
-
127
- # Directionality
128
- primary_direction: str
129
- has_bidi: bool
130
- rtl_ratio: float
131
-
132
- # Noise indicators
133
- noise_score: float
134
- has_zero_width: bool
135
- has_control_chars: bool
136
- has_excessive_punct: bool
137
- url_count: int
138
- emoji_count: int
139
- latin_ratio: float # in otherwise Indic text → potential transliteration
140
-
141
- # Unicode statistics
142
- unicode_block_distribution: Dict[str, float]
143
- unique_chars: int
144
- char_entropy: float
145
-
146
- # Language hints
147
- script_to_lang_hints: Dict[str, List[str]]
148
-
149
- def to_dict(self) -> Dict:
150
- return {
151
- "total_chars": self.total_chars,
152
- "alpha_chars": self.alpha_chars,
153
- "alpha_ratio": self.alpha_chars / max(self.total_chars, 1),
154
- "dominant_script": self.dominant_script,
155
- "dominant_script_ratio": self.dominant_script_ratio,
156
- "is_mixed_script": self.is_mixed_script,
157
- "mixing_score": self.mixing_score,
158
- "script_transitions": self.script_transitions,
159
- "primary_direction": self.primary_direction,
160
- "has_bidi": self.has_bidi,
161
- "noise_score": self.noise_score,
162
- "char_entropy": self.char_entropy,
163
- "script_distribution": self.script_distribution,
164
- "script_to_lang_hints": self.script_to_lang_hints,
165
- }
166
-
167
-
168
- # ─────────────────────────────────────────────
169
- # Script Analyser
170
- # ─────────────────────────────────────────────
171
-
172
- class ScriptAnalyzer:
173
- """
174
- Deep script and Unicode analysis for multilingual text.
175
- Designed for Indic languages with mixed-script handling.
176
- """
177
-
178
- _URL_RE = re.compile(r"https?://\S+|www\.\S+")
179
- _EMOJI_RE = re.compile(
180
- "["
181
- "\U0001F600-\U0001F64F"
182
- "\U0001F300-\U0001F5FF"
183
- "\U0001F680-\U0001F6FF"
184
- "\U0001F1E0-\U0001F1FF"
185
- "\U00002702-\U000027B0"
186
- "\U000024C2-\U0001F251"
187
- "]+",
188
- re.UNICODE
189
- )
190
- _ZW_RE = re.compile(r"[\u200b-\u200f\u202a-\u202e\ufeff\u00ad]")
191
- _CTRL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
192
-
193
- # Script → language hints
194
- SCRIPT_LANG_HINTS: Dict[str, List[str]] = {
195
- "Devanagari": ["hin", "mar"],
196
- "Bengali": ["ben"],
197
- "Gurmukhi": ["pan"],
198
- "Gujarati": ["guj"],
199
- "Oriya": ["ori"],
200
- "Tamil": ["tam"],
201
- "Telugu": ["tel"],
202
- "Kannada": ["kan"],
203
- "Malayalam": ["mal"],
204
- "Arabic": ["urd"],
205
- "Latin": ["eng"],
206
- }
207
-
208
- def analyze(self, text: str) -> ScriptAnalysisResult:
209
- """Comprehensive script analysis of input text."""
210
- if not text:
211
- return self._empty_result(text)
212
-
213
- chars = list(text)
214
- n = len(chars)
215
-
216
- # Character category counts
217
- alpha = sum(c.isalpha() for c in chars)
218
- digits = sum(c.isdigit() for c in chars)
219
- spaces = sum(c.isspace() for c in chars)
220
- puncts = sum(unicodedata.category(c).startswith("P") for c in chars)
221
- other = n - alpha - digits - spaces - puncts
222
-
223
- # Script assignment per alpha char
224
- script_of: List[Optional[str]] = [self._assign_script(c) for c in chars]
225
- alpha_scripts = [s for c, s in zip(chars, script_of) if c.isalpha() and s]
226
-
227
- # Script distribution
228
- script_counter = Counter(alpha_scripts)
229
- total_alpha = max(len(alpha_scripts), 1)
230
- script_dist = {s: c / total_alpha for s, c in script_counter.most_common()}
231
-
232
- dominant = script_counter.most_common(1)[0][0] if script_counter else "Unknown"
233
- dom_ratio = script_dist.get(dominant, 0.0)
234
-
235
- # Segment extraction
236
- segments = self._extract_segments(chars, script_of)
237
- transitions = max(0, len(segments) - 1)
238
-
239
- # Mixing score: entropy-based
240
- mixing_score = self._script_entropy(script_dist)
241
-
242
- # Directionality
243
- rtl_langs = {"Arabic", "Hebrew"}
244
- rtl_chars = sum(1 for s in alpha_scripts if s in rtl_langs)
245
- rtl_ratio = rtl_chars / max(total_alpha, 1)
246
- primary_direction = "RTL" if rtl_ratio > 0.5 else "LTR"
247
- has_bidi = 0.05 < rtl_ratio < 0.95
248
-
249
- # Unicode block distribution
250
- block_counter: Counter = Counter()
251
- for c in chars:
252
- block = get_unicode_block(ord(c))
253
- block_counter[block] += 1
254
- block_dist = {b: c / n for b, c in block_counter.most_common(10)}
255
-
256
- # Noise analysis
257
- url_count = len(self._URL_RE.findall(text))
258
- emoji_count = len(self._EMOJI_RE.findall(text))
259
- has_zero_width = bool(self._ZW_RE.search(text))
260
- has_control = bool(self._CTRL_RE.search(text))
261
- has_excessive_punct = (puncts / max(n, 1)) > 0.15
262
- latin_ratio = script_dist.get("Basic Latin", 0.0) + script_dist.get("Latin", 0.0)
263
-
264
- noise_components = [
265
- 0.3 * min(1.0, url_count * 0.5),
266
- 0.2 * int(has_zero_width),
267
- 0.2 * int(has_control),
268
- 0.15 * int(has_excessive_punct),
269
- 0.15 * min(1.0, emoji_count * 0.2),
270
- ]
271
- noise_score = sum(noise_components)
272
-
273
- # Char entropy
274
- char_freq = Counter(text)
275
- total_f = sum(char_freq.values())
276
- entropy = -sum(
277
- (c / total_f) * np.log2(c / total_f)
278
- for c in char_freq.values() if c > 0
279
- )
280
-
281
- # Lang hints based on detected scripts
282
- lang_hints: Dict[str, List[str]] = {
283
- s: self.SCRIPT_LANG_HINTS.get(s, [])
284
- for s in script_dist
285
- }
286
-
287
- return ScriptAnalysisResult(
288
- text=text,
289
- total_chars=n,
290
- alpha_chars=alpha,
291
- digit_chars=digits,
292
- punct_chars=puncts,
293
- space_chars=spaces,
294
- other_chars=other,
295
- script_distribution=script_dist,
296
- dominant_script=dominant,
297
- dominant_script_ratio=dom_ratio,
298
- is_mixed_script=len(script_dist) > 1 and dom_ratio < 0.95,
299
- mixing_score=mixing_score,
300
- script_segments=segments,
301
- script_transitions=transitions,
302
- primary_direction=primary_direction,
303
- has_bidi=has_bidi,
304
- rtl_ratio=rtl_ratio,
305
- noise_score=noise_score,
306
- has_zero_width=has_zero_width,
307
- has_control_chars=has_control,
308
- has_excessive_punct=has_excessive_punct,
309
- url_count=url_count,
310
- emoji_count=emoji_count,
311
- latin_ratio=latin_ratio,
312
- unicode_block_distribution=block_dist,
313
- unique_chars=len(set(text)),
314
- char_entropy=float(entropy),
315
- script_to_lang_hints=lang_hints,
316
- )
317
-
318
- def analyze_batch(self, texts: List[str]) -> List[ScriptAnalysisResult]:
319
- return [self.analyze(t) for t in texts]
320
-
321
- def _assign_script(self, char: str) -> Optional[str]:
322
- if not char.isalpha():
323
- return None
324
- cp = ord(char)
325
- for block_start, block_end, block_name in UNICODE_BLOCK_NAMES:
326
- if block_start <= cp <= block_end:
327
- return block_name
328
- return "Unknown"
329
-
330
- def _extract_segments(
331
- self, chars: List[str], scripts: List[Optional[str]]
332
- ) -> List[ScriptSegment]:
333
- """Extract contiguous script segments from text."""
334
- segments = []
335
- if not chars:
336
- return segments
337
-
338
- current_script = None
339
- start = 0
340
- seg_chars = []
341
-
342
- for i, (c, s) in enumerate(zip(chars, scripts)):
343
- if not c.isalpha():
344
- seg_chars.append(c)
345
- continue
346
- if s != current_script:
347
- if current_script is not None and seg_chars:
348
- segments.append(ScriptSegment(
349
- script=current_script,
350
- start=start,
351
- end=i,
352
- text="".join(seg_chars),
353
- char_count=sum(1 for ch in seg_chars if ch.isalpha()),
354
- ))
355
- current_script = s
356
- start = i
357
- seg_chars = [c]
358
- else:
359
- seg_chars.append(c)
360
-
361
- if current_script and seg_chars:
362
- segments.append(ScriptSegment(
363
- script=current_script,
364
- start=start,
365
- end=len(chars),
366
- text="".join(seg_chars),
367
- char_count=sum(1 for ch in seg_chars if ch.isalpha()),
368
- ))
369
-
370
- return [s for s in segments if s.char_count >= 3]
371
-
372
- def _script_entropy(self, dist: Dict[str, float]) -> float:
373
- """Shannon entropy over script distribution (normalized to [0,1])."""
374
- if len(dist) <= 1:
375
- return 0.0
376
- probs = list(dist.values())
377
- entropy = -sum(p * np.log2(p) for p in probs if p > 0)
378
- max_entropy = np.log2(len(probs))
379
- return float(entropy / max_entropy) if max_entropy > 0 else 0.0
380
-
381
- def _empty_result(self, text: str) -> ScriptAnalysisResult:
382
- return ScriptAnalysisResult(
383
- text=text, total_chars=0, alpha_chars=0, digit_chars=0,
384
- punct_chars=0, space_chars=0, other_chars=0,
385
- script_distribution={}, dominant_script="Unknown",
386
- dominant_script_ratio=0.0, is_mixed_script=False,
387
- mixing_score=0.0, script_segments=[], script_transitions=0,
388
- primary_direction="LTR", has_bidi=False, rtl_ratio=0.0,
389
- noise_score=0.0, has_zero_width=False, has_control_chars=False,
390
- has_excessive_punct=False, url_count=0, emoji_count=0,
391
- latin_ratio=0.0, unicode_block_distribution={},
392
- unique_chars=0, char_entropy=0.0, script_to_lang_hints={},
393
- )
394
-
395
- def compare_texts(self, text1: str, text2: str) -> Dict:
396
- """Compare script profiles of two texts."""
397
- r1 = self.analyze(text1)
398
- r2 = self.analyze(text2)
399
-
400
- scripts1 = set(r1.script_distribution.keys())
401
- scripts2 = set(r2.script_distribution.keys())
402
- shared = scripts1 & scripts2
403
-
404
- jaccard = len(shared) / max(len(scripts1 | scripts2), 1)
405
-
406
- return {
407
- "script_overlap": list(shared),
408
- "jaccard_similarity": jaccard,
409
- "dominant_scripts_match": r1.dominant_script == r2.dominant_script,
410
- "text1_dominant": r1.dominant_script,
411
- "text2_dominant": r2.dominant_script,
412
- "text1_mixing_score": r1.mixing_score,
413
- "text2_mixing_score": r2.mixing_score,
414
- }
415
-
416
- def detect_transliteration(self, text: str) -> Dict:
417
- """
418
- Detect if text is a transliteration of an Indic language
419
- written in Latin script (Romanized Indic).
420
- """
421
- result = self.analyze(text)
422
-
423
- # If predominantly Latin but contains Indic vocabulary patterns
424
- latin_dominated = result.dominant_script in ("Basic Latin", "Latin Extended-A")
425
- has_indic_patterns = self._has_indic_romanization_patterns(text)
426
-
427
- # Common Romanization markers
428
- has_aspirates = bool(re.search(r"\b\w*(kh|gh|ch|jh|th|dh|ph|bh)\w*\b", text, re.I))
429
- has_retroflex = bool(re.search(r"\b\w*[ṭḍṇṣ]\w*\b", text))
430
- has_nasal_markers = bool(re.search(r"\b\w*(ng|ny|ñ|ṅ)\w*\b", text, re.I))
431
-
432
- is_likely_transliteration = (
433
- latin_dominated and (has_indic_patterns or has_aspirates)
434
- )
435
-
436
- return {
437
- "is_transliteration": is_likely_transliteration,
438
- "confidence": 0.8 if (is_likely_transliteration and has_aspirates) else 0.5,
439
- "evidence": {
440
- "latin_dominated": latin_dominated,
441
- "has_indic_patterns": has_indic_patterns,
442
- "has_aspirates": has_aspirates,
443
- "has_retroflex": has_retroflex,
444
- "has_nasal_markers": has_nasal_markers,
445
- }
446
- }
447
-
448
- def _has_indic_romanization_patterns(self, text: str) -> bool:
449
- """Common romanization patterns across Indic languages."""
450
- patterns = [
451
- r"\b(hai|hain|nahin|nahi|aur|yeh|woh|kya|kaun|kahan|kyun|mera|tera|apna)\b", # Hindi
452
- r"\b(amma|appa|anna|akka|vanakkam|nandri|romba|paaru|paar)\b", # Tamil
453
- r"\b(ahe|aahe|nahi|tumhi|amhi|tya|tyanche|mhanje)\b", # Marathi
454
- ]
455
- for p in patterns:
456
- if re.search(p, text, re.I):
457
- return True
458
- return False
459
-
460
-
461
- # Singleton for reuse
462
- default_analyzer = ScriptAnalyzer()