sinlib 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sinlib-0.2.2 → sinlib-0.2.4}/.readthedocs.yaml +1 -1
- {sinlib-0.2.2 → sinlib-0.2.4}/PKG-INFO +1 -1
- {sinlib-0.2.2 → sinlib-0.2.4}/pyproject.toml +1 -1
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/__init__.py +1 -1
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/spellcheck.py +98 -50
- {sinlib-0.2.2 → sinlib-0.2.4}/tests/test_spellcheck.py +15 -12
- {sinlib-0.2.2 → sinlib-0.2.4}/website/astro.config.mjs +2 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/examples/visualization.md +4 -1
- {sinlib-0.2.2 → sinlib-0.2.4}/.github/workflows/python-publish.yml +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/.github/workflows/test.yml +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/.gitignore +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/LICENSE +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/README.md +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/requirements-dev.txt +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/requirements.txt +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/encoding.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/romanize.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/subword.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/tokenizer.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/transliterate.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/__init__.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/chars.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/dataset_utils.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/model_utils.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/models/__init__.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/models/akshara_ngram.json +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/models/akshara_vocab.json +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/models/bigru_detector.pt +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/models/transliterator-checkpoint.pth +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/models/transliterator_model.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/preprocessing.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/src/sinlib/utils/visualize.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/tests/test_combined_detector.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/tests/test_phase3_enhancements.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/tests/test_romanizer.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/tests/test_tokenizer.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/tests/test_tokenizer_batch.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/tests/test_tokenizer_bos.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/tests/test_tokenizer_special_tokens.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/tests/test_transliterator.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/tests/test_visualize.py +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/.gitignore +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/.vscode/extensions.json +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/.vscode/launch.json +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/README.md +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/package-lock.json +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/package.json +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/public/favicon.svg +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/assets/houston.webp +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/assets/logo-dark.png +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/assets/logo-light.png +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/api/encoding.mdx +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/api/preprocessing.mdx +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/api/romanizer.mdx +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/api/spellcheck.mdx +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/api/tokenizer.mdx +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/api/visualization.mdx +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/examples/tokenization.md +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/examples/typo-correction.md +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/guides/spellcheck.mdx +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/guides/tokenization.mdx +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content/docs/index.mdx +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/content.config.ts +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/src/styles/custom.css +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/website/tsconfig.json +0 -0
- {sinlib-0.2.2 → sinlib-0.2.4}/welcome.png +0 -0
|
@@ -3,15 +3,15 @@ from __future__ import annotations
|
|
|
3
3
|
import math
|
|
4
4
|
import warnings
|
|
5
5
|
import json
|
|
6
|
+
import re
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from difflib import get_close_matches, SequenceMatcher
|
|
8
|
-
from
|
|
9
|
-
from typing import Dict, List, Optional, Set
|
|
9
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
|
|
13
13
|
from sinlib.tokenizer import Tokenizer
|
|
14
|
-
from sinlib.utils.preprocessing import download_hub_file, Filenames
|
|
14
|
+
from sinlib.utils.preprocessing import download_hub_file, Filenames, normalize_sinhala, process_text
|
|
15
15
|
|
|
16
16
|
# Optional PyTorch import for Bi-GRU sequence labeling
|
|
17
17
|
try:
|
|
@@ -42,8 +42,8 @@ class AksharaNGram:
|
|
|
42
42
|
return prob
|
|
43
43
|
|
|
44
44
|
def score_word(self, word: str) -> float:
|
|
45
|
-
|
|
46
|
-
tokens = process_text(
|
|
45
|
+
normalized = normalize_sinhala(word)
|
|
46
|
+
tokens = process_text(normalized)
|
|
47
47
|
if not tokens:
|
|
48
48
|
return -999.0
|
|
49
49
|
|
|
@@ -194,7 +194,7 @@ else:
|
|
|
194
194
|
# Phonological Confusion & Keyboard Layout Proximity
|
|
195
195
|
# ------------------------------------------------------------------
|
|
196
196
|
|
|
197
|
-
CONFUSING_PAIRS: Set[
|
|
197
|
+
CONFUSING_PAIRS: Set[Tuple[str, str]] = {
|
|
198
198
|
("න", "ණ"), ("ල", "ළ"), ("ස", "ශ"), ("ස", "ෂ"), ("ශ", "ෂ"), ("ර", "ල"),
|
|
199
199
|
("ත", "ථ"), ("ද", "ධ"), ("ට", "ඨ"), ("ඩ", "ඪ"), ("ප", "ඵ"), ("බ", "භ"),
|
|
200
200
|
("ක", "ඛ"), ("ග", "ඝ"), ("ච", "ඡ"), ("ජ", "ඣ"),
|
|
@@ -258,14 +258,16 @@ def are_characters_keyboard_adjacent(c1: str, c2: str) -> bool:
|
|
|
258
258
|
k1_w = WIJESEKERA_MAP.get(c1)
|
|
259
259
|
k2_w = WIJESEKERA_MAP.get(c2)
|
|
260
260
|
if k1_w and k2_w:
|
|
261
|
-
if
|
|
262
|
-
|
|
261
|
+
if k1_w.lower() != k2_w.lower():
|
|
262
|
+
if is_qwerty_adjacent(k1_w.lower(), k2_w.lower()):
|
|
263
|
+
return True
|
|
263
264
|
|
|
264
265
|
k1_p = PHONETIC_MAP.get(c1)
|
|
265
266
|
k2_p = PHONETIC_MAP.get(c2)
|
|
266
267
|
if k1_p and k2_p and k1_p != "" and k2_p != "":
|
|
267
|
-
if
|
|
268
|
-
|
|
268
|
+
if k1_p.lower() != k2_p.lower():
|
|
269
|
+
if is_qwerty_adjacent(k1_p.lower(), k2_p.lower()):
|
|
270
|
+
return True
|
|
269
271
|
|
|
270
272
|
return False
|
|
271
273
|
|
|
@@ -324,15 +326,16 @@ def weighted_levenshtein(s1: str, s2: str) -> float:
|
|
|
324
326
|
class TrieNode:
|
|
325
327
|
def __init__(self):
|
|
326
328
|
self.children: Dict[str, TrieNode] = {}
|
|
327
|
-
self.word: str = None
|
|
329
|
+
self.word: Optional[str] = None
|
|
328
330
|
|
|
329
331
|
class PhonologicalTrie:
|
|
330
332
|
def __init__(self):
|
|
331
333
|
self.root = TrieNode()
|
|
334
|
+
self._sub_cost_cache: Dict[Tuple[str, str], float] = {}
|
|
332
335
|
|
|
333
336
|
def insert(self, word: str):
|
|
334
|
-
|
|
335
|
-
tokens = process_text(
|
|
337
|
+
normalized = normalize_sinhala(word)
|
|
338
|
+
tokens = process_text(normalized)
|
|
336
339
|
if not tokens:
|
|
337
340
|
return
|
|
338
341
|
curr = self.root
|
|
@@ -343,7 +346,25 @@ class PhonologicalTrie:
|
|
|
343
346
|
curr.word = word
|
|
344
347
|
|
|
345
348
|
def _get_substitution_cost(self, c1: str, c2: str) -> float:
|
|
346
|
-
|
|
349
|
+
if c1 == c2:
|
|
350
|
+
return 0.0
|
|
351
|
+
cache_key = (c1, c2)
|
|
352
|
+
if cache_key in self._sub_cost_cache:
|
|
353
|
+
return self._sub_cost_cache[cache_key]
|
|
354
|
+
|
|
355
|
+
DIACRITICS = {"ා", "ැ", "ෑ", "ි", "ී", "ු", "ූ", "ෘ", "ෲ", "ෙ", "ේ", "ෛ", "ො", "ෝ", "ෞ", "ං", "ඃ", "්"}
|
|
356
|
+
if (c1, c2) in CONFUSING_PAIRS or (c2, c1) in CONFUSING_PAIRS:
|
|
357
|
+
cost = 0.3
|
|
358
|
+
elif are_characters_keyboard_adjacent(c1, c2):
|
|
359
|
+
cost = 0.5
|
|
360
|
+
elif c1 in DIACRITICS and c2 in DIACRITICS:
|
|
361
|
+
cost = 0.4
|
|
362
|
+
else:
|
|
363
|
+
cost = 1.0
|
|
364
|
+
|
|
365
|
+
self._sub_cost_cache[cache_key] = cost
|
|
366
|
+
self._sub_cost_cache[(c2, c1)] = cost
|
|
367
|
+
return cost
|
|
347
368
|
|
|
348
369
|
def _search_recursive(self, node: TrieNode, token: str, previous_row: List[float],
|
|
349
370
|
search_tokens: List[str], max_cost: float, results: List[Tuple[float, str]]):
|
|
@@ -374,8 +395,8 @@ class PhonologicalTrie:
|
|
|
374
395
|
self._search_recursive(child_node, next_token, current_row, search_tokens, max_cost, results)
|
|
375
396
|
|
|
376
397
|
def search(self, word: str, max_cost: float = 1.5) -> List[Tuple[float, str]]:
|
|
377
|
-
|
|
378
|
-
search_tokens = process_text(
|
|
398
|
+
normalized = normalize_sinhala(word)
|
|
399
|
+
search_tokens = process_text(normalized)
|
|
379
400
|
if not search_tokens:
|
|
380
401
|
return []
|
|
381
402
|
|
|
@@ -449,6 +470,8 @@ class TypoDetector:
|
|
|
449
470
|
self.bigru_corrector: Optional[BiGRUSeq2Seq] = None
|
|
450
471
|
self.has_neural_corrector: bool = False
|
|
451
472
|
|
|
473
|
+
self._suggestion_cache: Dict[Tuple[str, int, Optional[str], Optional[str]], List[str]] = {}
|
|
474
|
+
|
|
452
475
|
if not lazy_loading:
|
|
453
476
|
self._ensure_loaded()
|
|
454
477
|
|
|
@@ -598,19 +621,20 @@ class TypoDetector:
|
|
|
598
621
|
2. Bi-GRU Sequence Labeler validity scores.
|
|
599
622
|
"""
|
|
600
623
|
self._ensure_loaded()
|
|
624
|
+
normalized_word = normalize_sinhala(word)
|
|
601
625
|
|
|
602
626
|
# 1. Akshara Trigram Score Check via word_ngram_probability
|
|
603
|
-
prob = self.word_ngram_probability(
|
|
627
|
+
prob = self.word_ngram_probability(normalized_word)
|
|
604
628
|
if prob < self._threshold:
|
|
605
629
|
return True
|
|
606
630
|
|
|
607
631
|
# 2. PyTorch Bi-GRU Sequence Labeler Check
|
|
608
|
-
if self.has_neural_labeler and self._bigru_detector is not None:
|
|
609
|
-
|
|
610
|
-
tokens = process_text(word)
|
|
632
|
+
if self.has_neural_labeler and self._bigru_detector is not None and self._akshara_vocab:
|
|
633
|
+
tokens = process_text(normalized_word)
|
|
611
634
|
if not tokens:
|
|
612
635
|
return True
|
|
613
|
-
|
|
636
|
+
unk_id = self._akshara_vocab.get("<UNK>", 0)
|
|
637
|
+
input_ids = [self._akshara_vocab.get(t, unk_id) for t in tokens]
|
|
614
638
|
input_tensor = torch.tensor([input_ids], dtype=torch.long)
|
|
615
639
|
with torch.no_grad():
|
|
616
640
|
probs = self._bigru_detector(input_tensor)[0].tolist()
|
|
@@ -620,9 +644,9 @@ class TypoDetector:
|
|
|
620
644
|
|
|
621
645
|
return False
|
|
622
646
|
|
|
623
|
-
@lru_cache(maxsize=1000)
|
|
624
647
|
def word_ngram_probability(self, word: str, n: int = 2) -> float:
|
|
625
648
|
self._ensure_loaded()
|
|
649
|
+
word = normalize_sinhala(word)
|
|
626
650
|
if len(self._akshara_ngram.vocab) == 0:
|
|
627
651
|
token_ids = self._tokenizer.encode(word)
|
|
628
652
|
prob = 1.0
|
|
@@ -635,34 +659,37 @@ class TypoDetector:
|
|
|
635
659
|
# Shift log probability score dynamically to map default threshold 1e-8 to -3.2 log-prob
|
|
636
660
|
return math.pow(10, score - 4.8)
|
|
637
661
|
|
|
662
|
+
|
|
638
663
|
def get_context_neg_log_prob(self, prev_word: Optional[str], candidate: str, next_word: Optional[str]) -> float:
|
|
639
664
|
"""Calculate cumulative negative log probability of candidate using Stupid Backoff."""
|
|
640
665
|
neg_log = 0.0
|
|
641
666
|
default_unigram_prob = 1e-8
|
|
642
|
-
|
|
667
|
+
cand_norm = normalize_sinhala(candidate)
|
|
668
|
+
p_candidate = self.news_unigrams.get(cand_norm, self.news_unigrams.get(candidate, default_unigram_prob)) if self.news_unigrams else default_unigram_prob
|
|
643
669
|
|
|
644
670
|
if prev_word:
|
|
645
|
-
|
|
671
|
+
prev_norm = normalize_sinhala(prev_word)
|
|
672
|
+
bigram1 = f"{prev_norm} {cand_norm}"
|
|
646
673
|
if self.news_bigrams and bigram1 in self.news_bigrams:
|
|
647
674
|
prob1 = self.news_bigrams[bigram1]
|
|
648
675
|
else:
|
|
649
676
|
prob1 = 0.4 * p_candidate
|
|
650
|
-
neg_log += -math.log10(prob1)
|
|
677
|
+
neg_log += -math.log10(max(prob1, 1e-12))
|
|
651
678
|
|
|
652
679
|
if next_word:
|
|
653
|
-
|
|
680
|
+
next_norm = normalize_sinhala(next_word)
|
|
681
|
+
bigram2 = f"{cand_norm} {next_norm}"
|
|
654
682
|
if self.news_bigrams and bigram2 in self.news_bigrams:
|
|
655
683
|
prob2 = self.news_bigrams[bigram2]
|
|
656
684
|
else:
|
|
657
685
|
prob2 = 0.4 * p_candidate
|
|
658
|
-
neg_log += -math.log10(prob2)
|
|
686
|
+
neg_log += -math.log10(max(prob2, 1e-12))
|
|
659
687
|
|
|
660
688
|
if not prev_word and not next_word:
|
|
661
|
-
neg_log = -math.log10(p_candidate)
|
|
689
|
+
neg_log = -math.log10(max(p_candidate, 1e-12))
|
|
662
690
|
|
|
663
691
|
return neg_log
|
|
664
692
|
|
|
665
|
-
@lru_cache(maxsize=1000)
|
|
666
693
|
def suggest_correction(self, word: str, n: int = 3, prev_word: Optional[str] = None,
|
|
667
694
|
next_word: Optional[str] = None) -> List[str]:
|
|
668
695
|
"""
|
|
@@ -670,7 +697,12 @@ class TypoDetector:
|
|
|
670
697
|
and Stupid Backoff context ranking.
|
|
671
698
|
"""
|
|
672
699
|
self._ensure_loaded()
|
|
673
|
-
|
|
700
|
+
word = normalize_sinhala(word)
|
|
701
|
+
|
|
702
|
+
cache_key = (word, n, prev_word, next_word)
|
|
703
|
+
if cache_key in self._suggestion_cache:
|
|
704
|
+
return self._suggestion_cache[cache_key]
|
|
705
|
+
|
|
674
706
|
candidates = {} # Map candidate_word -> edit_distance_cost
|
|
675
707
|
|
|
676
708
|
# 1. Coarse filter using difflib to guarantee backward/mock compatibility
|
|
@@ -687,18 +719,18 @@ class TypoDetector:
|
|
|
687
719
|
# 3. Generative Neural Corrector (only when dictionary is fully loaded)
|
|
688
720
|
if self.has_neural_corrector and self.bigru_corrector is not None and isinstance(self._dictionary, set) and len(self._dictionary) > 100:
|
|
689
721
|
try:
|
|
690
|
-
from sinlib.utils.preprocessing import process_text
|
|
691
722
|
tokens = process_text(word)
|
|
692
723
|
if tokens:
|
|
693
724
|
bos_idx = self._akshara_vocab.get("<BOS>", 1)
|
|
694
725
|
eos_idx = self._akshara_vocab.get("<EOS>", 2)
|
|
695
|
-
|
|
726
|
+
unk_id = self._akshara_vocab.get("<UNK>", 0)
|
|
727
|
+
input_ids = [self._akshara_vocab.get(t, unk_id) for t in tokens]
|
|
696
728
|
input_tensor = torch.tensor([input_ids], dtype=torch.long)
|
|
697
729
|
beams = self.bigru_corrector.beam_decode(input_tensor, bos_idx, eos_idx, beam_width=3, max_len=16)
|
|
698
730
|
|
|
699
731
|
id_to_tok = {v: k for k, v in self._akshara_vocab.items()}
|
|
700
732
|
for score, seq, _ in beams:
|
|
701
|
-
cand_tokens = [id_to_tok[idx] for idx in seq if idx not in (bos_idx, eos_idx, 0,
|
|
733
|
+
cand_tokens = [id_to_tok[idx] for idx in seq if idx not in (bos_idx, eos_idx, 0, unk_id)]
|
|
702
734
|
cand_word = "".join(cand_tokens)
|
|
703
735
|
if cand_word:
|
|
704
736
|
cost = weighted_levenshtein(word, cand_word)
|
|
@@ -709,7 +741,10 @@ class TypoDetector:
|
|
|
709
741
|
pass
|
|
710
742
|
|
|
711
743
|
if not candidates:
|
|
712
|
-
|
|
744
|
+
res = ["No suggestion"]
|
|
745
|
+
if len(self._suggestion_cache) < self._cache_size:
|
|
746
|
+
self._suggestion_cache[cache_key] = res
|
|
747
|
+
return res
|
|
713
748
|
|
|
714
749
|
# 4. Contextual Reranking using Stupid Backoff
|
|
715
750
|
scored = []
|
|
@@ -719,7 +754,17 @@ class TypoDetector:
|
|
|
719
754
|
scored.append((score, cand))
|
|
720
755
|
|
|
721
756
|
scored.sort(key=lambda x: x[0])
|
|
722
|
-
|
|
757
|
+
result = [cand for _, cand in scored[:n]]
|
|
758
|
+
if len(self._suggestion_cache) < self._cache_size:
|
|
759
|
+
self._suggestion_cache[cache_key] = result
|
|
760
|
+
return result
|
|
761
|
+
|
|
762
|
+
def _extract_punctuation(self, token: str) -> Tuple[str, str, str]:
|
|
763
|
+
"""Extract leading punctuation, core Sinhala word, and trailing punctuation from a token."""
|
|
764
|
+
match = re.match(r"^([^\w\u0D80-\u0DFF]*)([\w\u0D80-\u0DFF]+)([^\w\u0D80-\u0DFF]*)$", token, re.UNICODE)
|
|
765
|
+
if match:
|
|
766
|
+
return match.group(1), match.group(2), match.group(3)
|
|
767
|
+
return "", token, ""
|
|
723
768
|
|
|
724
769
|
def __call__(self, text: str) -> str:
|
|
725
770
|
"""
|
|
@@ -728,38 +773,41 @@ class TypoDetector:
|
|
|
728
773
|
"""
|
|
729
774
|
self._ensure_loaded()
|
|
730
775
|
corrected: List[str] = []
|
|
731
|
-
|
|
776
|
+
raw_tokens = text.split() if isinstance(text, str) else [str(text)]
|
|
777
|
+
parsed_tokens = [self._extract_punctuation(t) for t in raw_tokens]
|
|
778
|
+
words = [p[1] for p in parsed_tokens]
|
|
732
779
|
n_words = len(words)
|
|
733
780
|
|
|
734
|
-
for idx, word in enumerate(
|
|
781
|
+
for idx, (leading, word, trailing) in enumerate(parsed_tokens):
|
|
735
782
|
try:
|
|
736
|
-
if word
|
|
737
|
-
corrected.append(
|
|
783
|
+
if not word:
|
|
784
|
+
corrected.append(leading + trailing)
|
|
738
785
|
continue
|
|
739
786
|
|
|
740
|
-
|
|
787
|
+
norm_word = normalize_sinhala(word)
|
|
788
|
+
if norm_word in self._dictionary or word in self._dictionary:
|
|
789
|
+
corrected.append(leading + norm_word + trailing)
|
|
790
|
+
continue
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
if self.is_word_suspicious(norm_word):
|
|
741
794
|
prev_word = words[idx - 1] if idx > 0 else None
|
|
742
795
|
next_word = words[idx + 1] if idx < n_words - 1 else None
|
|
743
796
|
|
|
744
|
-
suggestions = self.suggest_correction(
|
|
797
|
+
suggestions = self.suggest_correction(norm_word, n=5, prev_word=prev_word, next_word=next_word)
|
|
745
798
|
if not suggestions or suggestions[0] == "No suggestion":
|
|
746
|
-
corrected.append(word)
|
|
799
|
+
corrected.append(leading + word + trailing)
|
|
747
800
|
else:
|
|
748
|
-
corrected.append(suggestions[0])
|
|
801
|
+
corrected.append(leading + suggestions[0] + trailing)
|
|
749
802
|
else:
|
|
750
|
-
|
|
751
|
-
f"'{word}' is unusual but may not be a typo.",
|
|
752
|
-
UserWarning,
|
|
753
|
-
stacklevel=2,
|
|
754
|
-
)
|
|
755
|
-
corrected.append(word)
|
|
803
|
+
corrected.append(leading + word + trailing)
|
|
756
804
|
|
|
757
805
|
except Exception as exc:
|
|
758
806
|
warnings.warn(
|
|
759
807
|
f"Error processing word '{word}': {exc}",
|
|
760
808
|
stacklevel=2,
|
|
761
809
|
)
|
|
762
|
-
corrected.append(word)
|
|
810
|
+
corrected.append(leading + word + trailing)
|
|
763
811
|
|
|
764
812
|
return " ".join(corrected)
|
|
765
813
|
|
|
@@ -141,15 +141,18 @@ def test_check_spelling_typo(mock_typo_detector):
|
|
|
141
141
|
assert result == "correct"
|
|
142
142
|
|
|
143
143
|
|
|
144
|
-
def
|
|
145
|
-
"""Test
|
|
146
|
-
mock_typo_detector._dictionary =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
144
|
+
def test_check_spelling_with_punctuation(mock_typo_detector):
|
|
145
|
+
"""Test that punctuation attached to words is preserved."""
|
|
146
|
+
mock_typo_detector._dictionary = {"ගෙදර", "පාසල"}
|
|
147
|
+
result = mock_typo_detector("මම ගෙදර. පාසල,")
|
|
148
|
+
assert "ගෙදර." in result
|
|
149
|
+
assert "පාසල," in result
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_unicode_normalization_in_spellcheck(mock_typo_detector):
|
|
153
|
+
"""Test that composite variant diacritics are normalized and matched against dictionary."""
|
|
154
|
+
mock_typo_detector._dictionary = {"කොළඹ"}
|
|
155
|
+
mock_typo_detector.trie = None
|
|
156
|
+
# "කොළඹ" uses composite variant ෙ + ා instead of ො
|
|
157
|
+
result = mock_typo_detector("කොළඹ")
|
|
158
|
+
assert result == "කොළඹ"
|
|
@@ -69,6 +69,7 @@ export default defineConfig({
|
|
|
69
69
|
{ label: 'Romanizer', slug: 'api/romanizer' },
|
|
70
70
|
{ label: 'BatchEncoding', slug: 'api/encoding' },
|
|
71
71
|
{ label: 'Preprocessing', slug: 'api/preprocessing' },
|
|
72
|
+
{ label: 'Visualization', slug: 'api/visualization' },
|
|
72
73
|
],
|
|
73
74
|
},
|
|
74
75
|
{
|
|
@@ -76,6 +77,7 @@ export default defineConfig({
|
|
|
76
77
|
items: [
|
|
77
78
|
{ label: 'Tokenization', slug: 'examples/tokenization' },
|
|
78
79
|
{ label: 'Typo Correction', slug: 'examples/typo-correction' },
|
|
80
|
+
{ label: 'Visualization', slug: 'examples/visualization' },
|
|
79
81
|
],
|
|
80
82
|
},
|
|
81
83
|
],
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Sinhala Text Visualization
|
|
3
|
+
description: Example of Matplotlib Sinhala text visualization with sinlib.
|
|
4
|
+
---
|
|
2
5
|
|
|
3
6
|
`sinlib` provides a zero-configuration utility `setup_matplotlib()` to resolve and configure Sinhala text rendering in Matplotlib.
|
|
4
7
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|