amharicNLP 0.0.0__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.
- amharicnlp-0.0.0/LICENSE +1 -0
- amharicnlp-0.0.0/MANIFEST.in +0 -0
- amharicnlp-0.0.0/PKG-INFO +5 -0
- amharicnlp-0.0.0/README.md +22 -0
- amharicnlp-0.0.0/amharicNLP/__init__.py +4 -0
- amharicnlp-0.0.0/amharicNLP/example/__init__.py +1 -0
- amharicnlp-0.0.0/amharicNLP/example/test.py +54 -0
- amharicnlp-0.0.0/amharicNLP/resources/__init__.py +9 -0
- amharicnlp-0.0.0/amharicNLP/resources/cleaner.py +58 -0
- amharicnlp-0.0.0/amharicNLP/resources/lemmatizer.py +84 -0
- amharicnlp-0.0.0/amharicNLP/resources/normalizer.py +300 -0
- amharicnlp-0.0.0/amharicNLP/resources/stemmer.py +85 -0
- amharicnlp-0.0.0/amharicNLP/resources/stopwrod.py +38 -0
- amharicnlp-0.0.0/amharicNLP/resources/tests/__init__.py +0 -0
- amharicnlp-0.0.0/amharicNLP/resources/tests/conftest.py +14 -0
- amharicnlp-0.0.0/amharicNLP/resources/tests/test_cleaner.py +14 -0
- amharicnlp-0.0.0/amharicNLP/resources/tests/test_lemmatizer.py +0 -0
- amharicnlp-0.0.0/amharicNLP/resources/tests/test_normalizer.py +6 -0
- amharicnlp-0.0.0/amharicNLP/resources/tests/test_utils.py +7 -0
- amharicnlp-0.0.0/amharicNLP/resources/tokenizer.py +64 -0
- amharicnlp-0.0.0/amharicNLP/resources/utils.py +21 -0
- amharicnlp-0.0.0/amharicNLP/utilities/__init__.py +1 -0
- amharicnlp-0.0.0/amharicNLP/utilities/data_augmenter.py +61 -0
- amharicnlp-0.0.0/amharicNLP/wordnet/__init__.py +14 -0
- amharicnlp-0.0.0/amharicNLP/wordnet/loader.py +10 -0
- amharicnlp-0.0.0/amharicNLP.egg-info/PKG-INFO +5 -0
- amharicnlp-0.0.0/amharicNLP.egg-info/SOURCES.txt +30 -0
- amharicnlp-0.0.0/amharicNLP.egg-info/dependency_links.txt +1 -0
- amharicnlp-0.0.0/amharicNLP.egg-info/top_level.txt +1 -0
- amharicnlp-0.0.0/pyproject.toml +1 -0
- amharicnlp-0.0.0/setup.cfg +4 -0
- amharicnlp-0.0.0/setup.py +5 -0
amharicnlp-0.0.0/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT License
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# đŞđš Amharic NLP Toolkit
|
|
2
|
+
|
|
3
|
+
A lightweight and easy-to-use Natural Language Processing (NLP) toolkit for the Amharic language.
|
|
4
|
+
Includes tokenization, normalization, and sentiment analysis tools.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## ⨠Features
|
|
9
|
+
|
|
10
|
+
- đ Amharic Text Tokenization
|
|
11
|
+
- đ§š Unicode Normalization & Cleaning
|
|
12
|
+
- đ Simple Sentiment Analysis (positive/negative)
|
|
13
|
+
- đ§° Command Line Interface (CLI) support
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## đŚ Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/yonasab12/amharic-nlp.git
|
|
21
|
+
cd amharic-nlp
|
|
22
|
+
pip install .
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from.test import *
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from amharicNLP.resources.cleaner import AmharicCleaner
|
|
4
|
+
from amharicNLP.resources.normalizer import AmharicNormalizer
|
|
5
|
+
from amharicNLP.resources.lemmatizer import AmharicLemmatizer
|
|
6
|
+
from amharicNLP.resources.stemmer import AmharicStemmer
|
|
7
|
+
from amharicNLP.resources.stopwrod import AmharicStopwordProcessor
|
|
8
|
+
from amharicNLP.resources.tokenizer import AmharicWordTokenizer
|
|
9
|
+
|
|
10
|
+
# Sample Amharic text
|
|
11
|
+
sample_text = "á á ááŤá˝á á˘áľáŽáľáŤ <h1/> ďż˝á áŤá á°ááŞáá˝ á áľáá
ááľ áá áľáŠá¨áľ ááľá¨á á ááŁá¸áᢠ123 ááĽá! á áá ááá ááá á¨á˘áľáŽáľáŤ á áá á áá áá˘"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Initialize all processors
|
|
15
|
+
cleaner = AmharicCleaner()
|
|
16
|
+
normalizer = AmharicNormalizer()
|
|
17
|
+
lemmatizer = AmharicLemmatizer()
|
|
18
|
+
stemmer = AmharicStemmer()
|
|
19
|
+
stopword_processor = AmharicStopwordProcessor()
|
|
20
|
+
|
|
21
|
+
# Apply each processing step
|
|
22
|
+
print("Original Text:", sample_text)
|
|
23
|
+
|
|
24
|
+
# 1. Clean the text
|
|
25
|
+
cleaned_texth = cleaner.remove_html(sample_text)
|
|
26
|
+
|
|
27
|
+
print("\nAfter HTML Removal:", cleaned_texth)
|
|
28
|
+
cleaned_textn = cleaner.remove_noise(cleaned_texth)
|
|
29
|
+
print("\nAfter Cleaning:", cleaned_textn)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# 2. Normalize the text
|
|
35
|
+
normalized_text = normalizer.normalize_amharic_chars(cleaned_textn)
|
|
36
|
+
normalized_text2=normalizer.normalize_punctuation_spacing(normalized_text)
|
|
37
|
+
|
|
38
|
+
normalized_text3=normalizer.expand_abbreviations(normalized_text2)
|
|
39
|
+
print("\nAfter Normalization:", normalized_text3)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# 4. Stopword removal
|
|
44
|
+
without_stopwords = stopword_processor.remove_stopwords(normalized_text3)
|
|
45
|
+
print("\nAfter Stopword Removal:", without_stopwords)
|
|
46
|
+
|
|
47
|
+
stem= stemmer.stem_amharic(without_stopwords)
|
|
48
|
+
print("\nAfter Stemming:", stem)
|
|
49
|
+
# 5. Lemmatization
|
|
50
|
+
#lemmatized_text = [lemmatizer.lemmatize(word) for word in without_stopwords.split()]
|
|
51
|
+
#print("\nAfter Lemmatization:", " ".join(lemmatized_text))
|
|
52
|
+
|
|
53
|
+
# 6. Stemming
|
|
54
|
+
##print("\nAfter Stemming:", " ".join(stemmed_text))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .cleaner import AmharicCleaner
|
|
2
|
+
from .normalizer import AmharicNormalizer
|
|
3
|
+
from .lemmatizer import AmharicLemmatizer
|
|
4
|
+
from .stemmer import AmharicStemmer
|
|
5
|
+
from.utils import AmharicLanguageDetector
|
|
6
|
+
from .stopwrod import AmharicStopwordProcessor
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = ['AmharicCleaner', 'AmharicNormalizer', 'AmharicLemmatizer', 'AmharicStemmer','AmharicLanguageDetector', 'AmharicStopwordProcessor','AmharicLemmatizer']
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from.utils import AmharicLanguageDetector
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
class AmharicCleaner:
|
|
5
|
+
"""import re
|
|
6
|
+
from utils import is_amharic_char
|
|
7
|
+
A class for cleaning text by removing HTML tags, emojis, and non-Amharic characters.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
# Compile regex patterns once when the class is instantiated
|
|
12
|
+
self.emoji_pattern = re.compile("["
|
|
13
|
+
u"\U0001F600-\U0001F64F"
|
|
14
|
+
u"\U0001F300-\U0001F5FF"
|
|
15
|
+
u"\U0001F680-\U0001F6FF"
|
|
16
|
+
u"\U0001F1E0-\U0001F1FF"
|
|
17
|
+
u"\U00002500-\U00002BEF"
|
|
18
|
+
u"\U00002702-\U000027B0"
|
|
19
|
+
u"\U00002702-\U000027B0"
|
|
20
|
+
u"\U000024C2-\U0001F251"
|
|
21
|
+
u"\U0001f926-\U0001f937"
|
|
22
|
+
u"\U00010000-\U0010ffff"
|
|
23
|
+
u"\u2640-\u2642"
|
|
24
|
+
u"\u2600-\u2B55"
|
|
25
|
+
u"\u200d"
|
|
26
|
+
u"\u23cf"
|
|
27
|
+
u"\u23e9"
|
|
28
|
+
u"\u231a"
|
|
29
|
+
u"\ufe0f"
|
|
30
|
+
u"\u3030"
|
|
31
|
+
"]+", flags=re.UNICODE)
|
|
32
|
+
|
|
33
|
+
self.html_pattern = re.compile(r'<.*?>')
|
|
34
|
+
|
|
35
|
+
def remove_html(self, text: str) -> str:
|
|
36
|
+
"""Remove HTML tags from text."""
|
|
37
|
+
return self.html_pattern.sub('', text)
|
|
38
|
+
|
|
39
|
+
def remove_noise(self, text: str, keep_amharic: bool = True) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Remove non-Amharic characters, emojis, and special symbols while preserving Amharic abbreviations.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
text: Input text to clean
|
|
45
|
+
keep_amharic: If True, keeps only Amharic characters, numbers, basic punctuation and abbreviations
|
|
46
|
+
"""
|
|
47
|
+
text = self.emoji_pattern.sub(r'', text)
|
|
48
|
+
|
|
49
|
+
if keep_amharic:
|
|
50
|
+
text = ''.join(char for char in text if (
|
|
51
|
+
AmharicLanguageDetector.is_amharic_char(char) or
|
|
52
|
+
char.isdigit() or
|
|
53
|
+
char in '.,!?;:- ' or
|
|
54
|
+
char in '/.'
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
text = ' '.join(text.split())
|
|
58
|
+
return text
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
class AmharicLemmatizer:
|
|
5
|
+
def __init__(self, wordnet_path=None):
|
|
6
|
+
# Handle direct dictionary input
|
|
7
|
+
if isinstance(wordnet_path, dict):
|
|
8
|
+
self.wordnet = wordnet_path
|
|
9
|
+
else:
|
|
10
|
+
# Handle case where wordnet_path might be a list
|
|
11
|
+
if isinstance(wordnet_path, list) and wordnet_path:
|
|
12
|
+
wordnet_path = wordnet_path[0] # Take first element if it's a non-empty list
|
|
13
|
+
|
|
14
|
+
if wordnet_path is None:
|
|
15
|
+
current_dir = os.path.dirname(__file__)
|
|
16
|
+
wordnet_path = os.path.join(current_dir, "..", "wordnet", "amh_wordnet.json")
|
|
17
|
+
|
|
18
|
+
# Validate path type
|
|
19
|
+
if not isinstance(wordnet_path, (str, bytes, os.PathLike)):
|
|
20
|
+
# Get the actual type name for better error message
|
|
21
|
+
actual_type = type(wordnet_path).__name__
|
|
22
|
+
raise TypeError(f"Invalid path type: {actual_type}. Expected string, path-like object, or dictionary.")
|
|
23
|
+
|
|
24
|
+
if not os.path.exists(wordnet_path):
|
|
25
|
+
raise FileNotFoundError(f"WordNet file not found at {wordnet_path}")
|
|
26
|
+
|
|
27
|
+
self.wordnet = self.load_wordnet(wordnet_path)
|
|
28
|
+
|
|
29
|
+
self.lemma_map = {}
|
|
30
|
+
self.fallback_map = {}
|
|
31
|
+
self.build_lemma_maps()
|
|
32
|
+
|
|
33
|
+
def load_wordnet(self, path):
|
|
34
|
+
# If we already have dictionary data, return it directly
|
|
35
|
+
if isinstance(path, dict):
|
|
36
|
+
return path
|
|
37
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
38
|
+
return json.load(f)
|
|
39
|
+
|
|
40
|
+
def build_lemma_maps(self):
|
|
41
|
+
# Ensure we're working with a list of synsets
|
|
42
|
+
if isinstance(self.wordnet, dict) and "synsets" in self.wordnet:
|
|
43
|
+
wordnet_data = self.wordnet["synsets"]
|
|
44
|
+
elif isinstance(self.wordnet, list):
|
|
45
|
+
wordnet_data = self.wordnet
|
|
46
|
+
else:
|
|
47
|
+
# Handle unexpected structure
|
|
48
|
+
wordnet_data = []
|
|
49
|
+
|
|
50
|
+
for synset in wordnet_data:
|
|
51
|
+
# Skip synsets without 'lemmas' key
|
|
52
|
+
if "lemmas" not in synset or not synset["lemmas"]:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
base_lemma = synset["lemmas"][0]
|
|
56
|
+
pos_tag = synset.get("pos", None)
|
|
57
|
+
|
|
58
|
+
for lemma in synset["lemmas"]:
|
|
59
|
+
normalized = lemma.lower()
|
|
60
|
+
|
|
61
|
+
# Add to POS-specific map if POS is available
|
|
62
|
+
if pos_tag:
|
|
63
|
+
pos_key = (normalized, pos_tag)
|
|
64
|
+
if pos_key not in self.lemma_map:
|
|
65
|
+
self.lemma_map[pos_key] = base_lemma
|
|
66
|
+
|
|
67
|
+
# Add to fallback map
|
|
68
|
+
if normalized not in self.fallback_map:
|
|
69
|
+
self.fallback_map[normalized] = base_lemma
|
|
70
|
+
|
|
71
|
+
def lemmatize(self, word, pos=None):
|
|
72
|
+
normalized = word.lower()
|
|
73
|
+
|
|
74
|
+
if pos is not None:
|
|
75
|
+
# Try POS-specific lookup
|
|
76
|
+
pos_key = (normalized, pos)
|
|
77
|
+
if pos_key in self.lemma_map:
|
|
78
|
+
return self.lemma_map[pos_key]
|
|
79
|
+
|
|
80
|
+
# Try fallback (POS=None)
|
|
81
|
+
if normalized in self.fallback_map:
|
|
82
|
+
return self.fallback_map[normalized]
|
|
83
|
+
|
|
84
|
+
return word
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import unicodedata
|
|
3
|
+
from typing import Dict, Optional, List, Tuple, Set
|
|
4
|
+
|
|
5
|
+
class AmharicNormalizer:
|
|
6
|
+
|
|
7
|
+
AMHARIC_NORMALIZATION_MAP = {
|
|
8
|
+
# Normalize variants of á series
|
|
9
|
+
'á': 'á', 'á
': 'á', 'á': 'á', 'á': 'á', 'á': 'á', 'áť': 'á',
|
|
10
|
+
'á': 'á', 'á': 'á', 'á
': 'á',
|
|
11
|
+
'á': 'á', 'á': 'á', 'áş': 'á',
|
|
12
|
+
'á': 'á', 'á': 'á', 'á': 'á',
|
|
13
|
+
'á': 'á
', 'á
': 'á
',
|
|
14
|
+
'á': 'á', 'á': 'á', 'áž': 'á',
|
|
15
|
+
|
|
16
|
+
# Normalize á° series
|
|
17
|
+
'á ': 'á°', 'áĄ': 'áą', 'á˘': 'á˛', 'áŁ': 'áł',
|
|
18
|
+
'á¤': 'á´', 'áĽ': 'áľ', 'áŚ': 'áś',
|
|
19
|
+
|
|
20
|
+
# Normalize á series
|
|
21
|
+
'á': 'á ', 'áŁ': 'á ', 'á': 'á ',
|
|
22
|
+
'á': 'áĄ', 'á': 'á˘', 'á': 'á¤', 'á': 'áĽ', 'á': 'áŚ',
|
|
23
|
+
|
|
24
|
+
# Normalize á series
|
|
25
|
+
'á¸': 'á', 'áš': 'á', 'áş': 'á', 'áť': 'á',
|
|
26
|
+
'áź': 'á', 'á˝': 'á
', 'áž': 'á',
|
|
27
|
+
|
|
28
|
+
# Other normalizations
|
|
29
|
+
'á': 'á', 'áľ': 'áŠ'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Labialized character components
|
|
33
|
+
LABIALIZED_COMPONENTS = {
|
|
34
|
+
'á': 'á', 'á': 'á', 'áą': 'áˇ', 'áŠ': 'áŻ', 'áą': 'áˇ', 'áš': 'áż',
|
|
35
|
+
'á': 'á', 'áĄ': 'á§', 'áš': 'áż', 'á': 'á', 'á': 'á', 'á': 'á',
|
|
36
|
+
'áŠ': 'áł', 'á': 'á', 'á': 'á', 'á°': 'áˇ', 'áĄ': 'á§', 'áŠ': 'áŻ',
|
|
37
|
+
'áš': 'áż', 'á': 'á'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Punctuation Handling
|
|
41
|
+
AMHARIC_PUNCTUATION = {'á˘', 'áŁ', 'á¤', 'áĽ', 'áŚ', 'áĄ'} # Amharic-specific punctuation
|
|
42
|
+
|
|
43
|
+
PUNCTUATION_NORMALIZATION = [
|
|
44
|
+
(re.compile(r'\.'), ' . '),
|
|
45
|
+
(re.compile(r'áĄ'), ' ᥠ'),
|
|
46
|
+
(re.compile(r'[!?]'), r' \g<0> '),
|
|
47
|
+
(re.compile(r'á˘'), ' ᢠ'),
|
|
48
|
+
(re.compile(r'áŁ'), ' ᣠ'),
|
|
49
|
+
(re.compile(r'á¤'), ' ᤠ'),
|
|
50
|
+
(re.compile(r'[-ââ]+'), r' \g<0> '),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Numerals
|
|
54
|
+
AMHARIC_NUMERALS = {
|
|
55
|
+
'áŠ': '1', 'áŞ': '2', 'áŤ': '3', 'áŹ': '4', 'á': '5',
|
|
56
|
+
'áŽ': '6', 'áŻ': '7', 'á°': '8', 'áą': '9', 'á˛': '10',
|
|
57
|
+
'áł': '20', 'á´': '30', 'áľ': '40', 'áś': '50',
|
|
58
|
+
'áˇ': '60', 'á¸': '70', 'áš': '80', 'áş': '90', 'áť': '100'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Spelling Corrections
|
|
62
|
+
COMMON_SPELLING_CORRECTIONS = {
|
|
63
|
+
'á á ': 'á á áŤ',
|
|
64
|
+
'á áá': 'á°áá',
|
|
65
|
+
# Add more common corrections here
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Abbreviations
|
|
69
|
+
AMHARIC_ABBREVIATIONS = {
|
|
70
|
+
'áľ/á¤áľ': 'áľáá
ááľ á¤áľ',
|
|
71
|
+
'áľ/ááľ': 'áľáá
ááľ',
|
|
72
|
+
'áľ/ááá': 'áľáá
ááľ ááá',
|
|
73
|
+
'á/á áá': 'áááł á áá',
|
|
74
|
+
'á/áľáá´': 'ááá áľáá´',
|
|
75
|
+
'á°/áááľ': 'á°áĽá¨ áááľ',
|
|
76
|
+
'á°/áłáŚá': 'á°áĽá¨ áłáŚá',
|
|
77
|
+
'á/á': 'ááá
á',
|
|
78
|
+
'á/á¤áľ': 'ááľáŞáŤ á¤áľ',
|
|
79
|
+
'á/á áá': 'ááś á áá',
|
|
80
|
+
'á/á¨á°á': 'ááá á¨á°á',
|
|
81
|
+
'á/ááá': 'ááá ááá',
|
|
82
|
+
'á/á': 'ááłá°á',
|
|
83
|
+
'á/áŽ': 'ááááŽ',
|
|
84
|
+
'á/áŞáľ': 'ááááŞáľ',
|
|
85
|
+
'á/áľáá´': 'ááá° áľáá´',
|
|
86
|
+
'á/áľáá´': 'áá
ᨠ�áľáá´',
|
|
87
|
+
'á/á¤áľ': 'áááľ á¤áľ',
|
|
88
|
+
'á˝/á¤áľ': 'á˝á
ááľ á¤áľ',
|
|
89
|
+
'á˛/á': 'á˛áľá°á',
|
|
90
|
+
'á /áááľáľá': 'á á
áá áááľáľá',
|
|
91
|
+
'áś/á': 'áśáá°á',
|
|
92
|
+
'á/ááŽáááľ': 'ááĽá¨ ááŽáááľ',
|
|
93
|
+
'á¤/áááľáľáŤá': 'á¤á° áááľáľáŤá',
|
|
94
|
+
'á/áľáŤ': 'áááľá áľáŤ',
|
|
95
|
+
'á/á¤áľ': 'ááá á¤áľ',
|
|
96
|
+
'á°/áááááľ': 'á°áá áááááľ',
|
|
97
|
+
'á/á': 'áááľáľá',
|
|
98
|
+
'áŽ/á': 'áŽááá',
|
|
99
|
+
'á/áááŤá': 'ááá áááŤá',
|
|
100
|
+
'áĽ/áááŤá': 'áĽáááłá áááŤá',
|
|
101
|
+
'á/áŽááá': 'áá°áááľ áŽááá',
|
|
102
|
+
'á/ááá á': 'áá ááá á',
|
|
103
|
+
'á /á ': 'á á˛áľ á á áŁ',
|
|
104
|
+
'á/ááá
á': 'ááĽá° ááá
á',
|
|
105
|
+
'á/áľ': 'ááŹááłááľ',
|
|
106
|
+
'á.á': 'á áá° áá
á¨áľ',
|
|
107
|
+
'á.á': 'á á˛áľ á á áŁ',
|
|
108
|
+
'áś.á': 'áśáá°á',
|
|
109
|
+
'á/á': 'ááŽáá°á'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# ====================== CORE FUNCTIONS ======================
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def normalize_unicode(text: str, form: str = 'NFC') -> str:
|
|
116
|
+
"""Normalize Unicode text to specified form (default: NFC)."""
|
|
117
|
+
return unicodedata.normalize(form, text)
|
|
118
|
+
|
|
119
|
+
def normalize_amharic_chars(self, text: str) -> str:
|
|
120
|
+
"""Normalize Amharic characters to their canonical forms with enhanced labialization handling."""
|
|
121
|
+
normalized = []
|
|
122
|
+
i = 0
|
|
123
|
+
n = len(text)
|
|
124
|
+
|
|
125
|
+
while i < n:
|
|
126
|
+
current_char = text[i]
|
|
127
|
+
|
|
128
|
+
# Check for labialized character patterns (base + á or á )
|
|
129
|
+
if i + 1 < n and current_char in self.LABIALIZED_COMPONENTS:
|
|
130
|
+
next_char = text[i+1]
|
|
131
|
+
if next_char in ['á', 'á ']:
|
|
132
|
+
normalized.append(self.LABIALIZED_COMPONENTS[current_char])
|
|
133
|
+
i += 2
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# Handle standard character mappings
|
|
137
|
+
normalized.append(self.AMHARIC_NORMALIZATION_MAP.get(current_char, current_char))
|
|
138
|
+
i += 1
|
|
139
|
+
|
|
140
|
+
return ''.join(normalized)
|
|
141
|
+
|
|
142
|
+
def normalize_punctuation_spacing(self, text: str) -> str:
|
|
143
|
+
"""Ensure proper spacing around punctuation marks."""
|
|
144
|
+
for pattern, replacement in self.PUNCTUATION_NORMALIZATION:
|
|
145
|
+
text = pattern.sub(replacement, text)
|
|
146
|
+
return text
|
|
147
|
+
|
|
148
|
+
def remove_punctuation(self, text: str,
|
|
149
|
+
keep_basic: bool = True,
|
|
150
|
+
keep_amharic: bool = True) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Enhanced punctuation removal with Amharic support.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
text: Input text
|
|
156
|
+
keep_basic: Keep basic punctuation (.!?,;:-)
|
|
157
|
+
keep_amharic: Keep Amharic-specific punctuation
|
|
158
|
+
"""
|
|
159
|
+
keep = []
|
|
160
|
+
if keep_basic:
|
|
161
|
+
keep.append(r'\.!?,;:-')
|
|
162
|
+
if keep_amharic:
|
|
163
|
+
keep.append(''.join(self.AMHARIC_PUNCTUATION))
|
|
164
|
+
|
|
165
|
+
if keep:
|
|
166
|
+
punctuation = rf'[^\w\s{"".join(keep)}]'
|
|
167
|
+
else:
|
|
168
|
+
punctuation = r'[^\w\s]'
|
|
169
|
+
|
|
170
|
+
return re.sub(punctuation, '', text)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def normalize_whitespace(text: str) -> str:
|
|
174
|
+
"""Normalize all whitespace characters to single space."""
|
|
175
|
+
text = re.sub(r'[\s\u2000-\u200F\u2028-\u202F\u205F\u3000]+', ' ', text)
|
|
176
|
+
return text.strip()
|
|
177
|
+
|
|
178
|
+
def normalize_numbers(self, text: str, to_western: bool = False) -> str:
|
|
179
|
+
"""
|
|
180
|
+
Normalize Amharic numbers to either Western or keep Amharic.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
to_western: If True, converts Amharic numbers to Western (0-9)
|
|
184
|
+
"""
|
|
185
|
+
if not to_western:
|
|
186
|
+
return text
|
|
187
|
+
|
|
188
|
+
return ''.join(self.AMHARIC_NUMERALS.get(char, char) for char in text)
|
|
189
|
+
|
|
190
|
+
def fix_common_spelling(self, text: str, custom_map: Optional[Dict[str, str]] = None) -> str:
|
|
191
|
+
"""
|
|
192
|
+
Fix common Amharic spelling variations.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
custom_map: Additional spelling corrections to apply
|
|
196
|
+
"""
|
|
197
|
+
corrections = self.COMMON_SPELLING_CORRECTIONS.copy()
|
|
198
|
+
if custom_map:
|
|
199
|
+
corrections.update(custom_map)
|
|
200
|
+
|
|
201
|
+
for wrong, correct in corrections.items():
|
|
202
|
+
text = text.replace(wrong, correct)
|
|
203
|
+
|
|
204
|
+
return text
|
|
205
|
+
|
|
206
|
+
def expand_abbreviations(self, text: str, custom_abbr: Optional[Dict[str, str]] = None) -> str:
|
|
207
|
+
"""
|
|
208
|
+
Expand Amharic abbreviations to their full forms.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
text: Input text containing abbreviations
|
|
212
|
+
custom_abbr: Additional abbreviations to expand
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Text with abbreviations expanded
|
|
216
|
+
"""
|
|
217
|
+
abbreviations = self.AMHARIC_ABBREVIATIONS.copy()
|
|
218
|
+
if custom_abbr:
|
|
219
|
+
abbreviations.update(custom_abbr)
|
|
220
|
+
|
|
221
|
+
# Sort by length to match longer abbreviations first
|
|
222
|
+
for abbr in sorted(abbreviations, key=len, reverse=True):
|
|
223
|
+
text = text.replace(abbr, abbreviations[abbr])
|
|
224
|
+
|
|
225
|
+
return text
|
|
226
|
+
|
|
227
|
+
# ====================== MAIN NORMALIZATION FUNCTION ======================
|
|
228
|
+
|
|
229
|
+
def normalize_text(
|
|
230
|
+
self,
|
|
231
|
+
text: str,
|
|
232
|
+
*,
|
|
233
|
+
unicode_form: str = 'NFC',
|
|
234
|
+
normalize_chars: bool = True,
|
|
235
|
+
normalize_punct_spacing: bool = True,
|
|
236
|
+
remove_punct: bool = False,
|
|
237
|
+
keep_basic_punct: bool = True,
|
|
238
|
+
keep_amharic_punct: bool = True,
|
|
239
|
+
normalize_ws: bool = True,
|
|
240
|
+
normalize_nums: bool = False,
|
|
241
|
+
fix_spelling: bool = False,
|
|
242
|
+
expand_abbr: bool = True,
|
|
243
|
+
custom_spelling_map: Optional[Dict[str, str]] = None,
|
|
244
|
+
custom_abbr_map: Optional[Dict[str, str]] = None
|
|
245
|
+
) -> str:
|
|
246
|
+
"""
|
|
247
|
+
Comprehensive Amharic text normalization pipeline.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
text: Input text to normalize
|
|
251
|
+
unicode_form: Unicode normalization form (NFC, NFD, NFKC, NFKD)
|
|
252
|
+
normalize_chars: Normalize Amharic character variants
|
|
253
|
+
normalize_punct_spacing: Add spaces around punctuation
|
|
254
|
+
remove_punct: Remove punctuation
|
|
255
|
+
keep_basic_punct: Keep basic punctuation if removing punctuation
|
|
256
|
+
keep_amharic_punct: Keep Amharic punctuation if removing punctuation
|
|
257
|
+
normalize_ws: Normalize whitespace
|
|
258
|
+
normalize_nums: Convert Amharic numbers to Western numerals
|
|
259
|
+
fix_spelling: Apply common spelling corrections
|
|
260
|
+
expand_abbr: Expand common Amharic abbreviations
|
|
261
|
+
custom_spelling_map: Custom spelling corrections dictionary
|
|
262
|
+
custom_abbr_map: Custom abbreviations dictionary
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Normalized text string
|
|
266
|
+
"""
|
|
267
|
+
# Normalize Unicode first
|
|
268
|
+
text = self.normalize_unicode(text, unicode_form)
|
|
269
|
+
|
|
270
|
+
# Expand abbreviations
|
|
271
|
+
if expand_abbr:
|
|
272
|
+
text = self.expand_abbreviations(text, custom_abbr_map)
|
|
273
|
+
|
|
274
|
+
# Normalize Amharic characters
|
|
275
|
+
if normalize_chars:
|
|
276
|
+
text = self.normalize_amharic_chars(text)
|
|
277
|
+
|
|
278
|
+
# Normalize punctuation spacing
|
|
279
|
+
if normalize_punct_spacing:
|
|
280
|
+
text = self.normalize_punctuation_spacing(text)
|
|
281
|
+
|
|
282
|
+
# Remove punctuation (if enabled)
|
|
283
|
+
if remove_punct:
|
|
284
|
+
text = self.remove_punctuation(text,
|
|
285
|
+
keep_basic=keep_basic_punct,
|
|
286
|
+
keep_amharic=keep_amharic_punct)
|
|
287
|
+
|
|
288
|
+
# Normalize whitespace
|
|
289
|
+
if normalize_ws:
|
|
290
|
+
text = self.normalize_whitespace(text)
|
|
291
|
+
|
|
292
|
+
# Normalize numbers
|
|
293
|
+
if normalize_nums:
|
|
294
|
+
text = self.normalize_numbers(text, to_western=True)
|
|
295
|
+
|
|
296
|
+
# Fix common spelling
|
|
297
|
+
if fix_spelling:
|
|
298
|
+
text = self.fix_common_spelling(text, custom_spelling_map)
|
|
299
|
+
|
|
300
|
+
return text
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
class AmharicStemmer:
|
|
2
|
+
# Comprehensive Amharic Stemmer
|
|
3
|
+
# Based on linguistic research from Addis Ababa University and Amharic NLP Toolkit
|
|
4
|
+
|
|
5
|
+
# Curated affix lists from academic sources
|
|
6
|
+
PREFIXES = [
|
|
7
|
+
# Subject markers
|
|
8
|
+
"áĽá", "á", "áľ", "áĽ", "á", "á°", "á á", "á áľ", "á á", "á",
|
|
9
|
+
"áł", "áá°", "á á", "á á", "á°á", "á ",
|
|
10
|
+
|
|
11
|
+
# Prepositions and conjunctions
|
|
12
|
+
"á¨", "á ", "á¨", "á", "áľá", "áĽáľá¨", "á áľá°", "á áŠá", "á ááľáĽ",
|
|
13
|
+
"á¨ááľáĽ", "áááłá", "áĽáá°", "áŤá", "á¨áá", "á áá",
|
|
14
|
+
|
|
15
|
+
# Verb derivation markers
|
|
16
|
+
"á áľ", "á°áľ", "á áľá°", "á", "á", "áł", "áľ", "áĽ", "á", "áľ",
|
|
17
|
+
"á", "á", "á", "áľ", "á
", "áľ", "á", "á", "áť", "áŤ", "áŤ",
|
|
18
|
+
"á", "á", "á
", "áť", "á", "áľ", "á"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
SUFFIXES = [
|
|
22
|
+
# Object markers
|
|
23
|
+
"á", "á
", "á˝", "á˝á", "áá¸á", "ááľ", "á á¸á", "á á˝áá",
|
|
24
|
+
"á§á¸á", "áŁá¸á", "áŁá˝á", "áŁá˝áá", "ááľ", "áá˝", "áá˝á",
|
|
25
|
+
|
|
26
|
+
# Tense/aspect markers
|
|
27
|
+
"á á", "á á", "áŁá", "áŁá", "á§á", "á§á", "á á˝á", "á á˝áá",
|
|
28
|
+
"á§á˝á", "á§á˝áá", "á á", "á§á", "áŁá", "á á˝á", "á§á", "á§á
",
|
|
29
|
+
"á§á˝", "á§á", "á§", "áŚ", "áĄ", "á˘", "á¤",
|
|
30
|
+
|
|
31
|
+
# Verb suffixes
|
|
32
|
+
"áá", "áá á", "á§á", "á§á", "á§á", "á§á", "á§á", "á§áá˝", "á§á",
|
|
33
|
+
"á§áŁáľ", "áŁáľ", "á§áá˝", "áŁáá˝", "áŁáá", "á§áá",
|
|
34
|
+
|
|
35
|
+
# Nominal suffixes
|
|
36
|
+
"á˝", "á", "á", "á", "áľ", "á", "á", "áá˝", "á", "áá", "á", "á¸á"
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# Words exempt from stemming
|
|
40
|
+
PROTECTED_WORDS = {
|
|
41
|
+
"ááľáĽ", "áá", "á áá", "á¨áłá˝", "ááľ", "áá", "áľááá",
|
|
42
|
+
"áľá", "áľáŤ", "á¤áľ", "á áŁáľ", "áĽááľ", "áá
", "áááľ", "á´áľ",
|
|
43
|
+
"áĽá", "á áá°", "á ááş", "áĽáą", "áĽáˇ", "áĽá", "áĽááá°", "áĽáłá¸á",
|
|
44
|
+
"á˘áľáŽáľáŤ", "á á˛áľ", "á á áŁ", "áá", "ááľá
", "á´áááŤá˛"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def __init__(self):
|
|
48
|
+
# Sort by length for longest-match-first
|
|
49
|
+
self.PREFIXES.sort(key=len, reverse=True)
|
|
50
|
+
self.SUFFIXES.sort(key=len, reverse=True)
|
|
51
|
+
|
|
52
|
+
def stem_amharic(self, word):
|
|
53
|
+
"""Stem Amharic words using morphological analysis"""
|
|
54
|
+
# Preserve protected words and short words
|
|
55
|
+
if word in self.PROTECTED_WORDS or len(word) <= 3:
|
|
56
|
+
return word
|
|
57
|
+
|
|
58
|
+
original = word
|
|
59
|
+
stem = word
|
|
60
|
+
|
|
61
|
+
# Remove prefixes (max 2 iterations)
|
|
62
|
+
for _ in range(2):
|
|
63
|
+
removed = False
|
|
64
|
+
for prefix in self.PREFIXES:
|
|
65
|
+
if stem.startswith(prefix) and len(stem) > len(prefix) + 2:
|
|
66
|
+
stem = stem[len(prefix):]
|
|
67
|
+
removed = True
|
|
68
|
+
break
|
|
69
|
+
if not removed:
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
# Remove suffixes (max 2 iterations)
|
|
73
|
+
for _ in range(2):
|
|
74
|
+
removed = False
|
|
75
|
+
for suffix in self.SUFFIXES:
|
|
76
|
+
if stem.endswith(suffix) and len(stem) > len(suffix) + 2:
|
|
77
|
+
stem = stem[:-len(suffix)]
|
|
78
|
+
removed = True
|
|
79
|
+
break
|
|
80
|
+
if not removed:
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
# Fallback to original if stem is invalid
|
|
84
|
+
return stem if len(stem) >= 3 else original
|
|
85
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
class AmharicStopwordProcessor:
|
|
5
|
+
def __init__(self, stopwords_file=None):
|
|
6
|
+
"""
|
|
7
|
+
Initialize stopword processor with optional custom stopwords file
|
|
8
|
+
:param stopwords_file: Optional path to custom stopwords file
|
|
9
|
+
"""
|
|
10
|
+
if stopwords_file is None:
|
|
11
|
+
# Automatically locate the stopwords file in package resources
|
|
12
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
stopwords_file = os.path.join(current_dir, "amharic_stopwords.txt")
|
|
14
|
+
|
|
15
|
+
self.stopwords = self.load_stopwords(stopwords_file)
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def load_stopwords(file_path):
|
|
19
|
+
"""Load stopwords from a text file."""
|
|
20
|
+
if not os.path.exists(file_path):
|
|
21
|
+
raise FileNotFoundError(f"Stopwords file not found: {file_path}")
|
|
22
|
+
|
|
23
|
+
with open(file_path, 'r', encoding='utf-8') as file:
|
|
24
|
+
return set(line.strip() for line in file)
|
|
25
|
+
|
|
26
|
+
def remove_stopwords(self, text):
|
|
27
|
+
"""
|
|
28
|
+
Remove Amharic stopwords from text
|
|
29
|
+
:param text: Input text to process
|
|
30
|
+
:return: Filtered text with stopwords removed
|
|
31
|
+
"""
|
|
32
|
+
# Tokenize while preserving Amharic characters
|
|
33
|
+
words = re.findall(r'[\w\u1200-\u137F]+', text, re.UNICODE)
|
|
34
|
+
|
|
35
|
+
# Filter out stopwords (case-insensitive)
|
|
36
|
+
filtered_words = [word for word in words if word.lower() not in self.stopwords]
|
|
37
|
+
|
|
38
|
+
return ' '.join(filtered_words)
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from amharicNLP.resources import AmharicCleaner, AmharicNormalizer
|
|
3
|
+
|
|
4
|
+
@pytest.fixture
|
|
5
|
+
def sample_amharic_text():
|
|
6
|
+
return "áá
á¨áá¨áŤ á˝áá ááá˘"
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def cleaner():
|
|
10
|
+
return AmharicCleaner()
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def normalizer():
|
|
14
|
+
return AmharicNormalizer()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from amharicNLP .resources.cleaner import AmharicCleaner
|
|
3
|
+
|
|
4
|
+
class TestAmharicCleaner:
|
|
5
|
+
@pytest.fixture
|
|
6
|
+
def cleaner(self):
|
|
7
|
+
return AmharicCleaner()
|
|
8
|
+
|
|
9
|
+
def test_remove_html(self, cleaner):
|
|
10
|
+
assert cleaner.remove_html("<p>Hello</p>") == "Hello"
|
|
11
|
+
|
|
12
|
+
def test_remove_noise(self, cleaner):
|
|
13
|
+
text = "á°áá! đ Hello 123"
|
|
14
|
+
assert cleaner.remove_noise(text) == "á°áá! 123"
|
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from amharicNLP.resources.utils import AmharicLanguageDetector
|
|
2
|
+
|
|
3
|
+
class TestAmharicLanguageDetector:
|
|
4
|
+
def test_is_amharic_text(self):
|
|
5
|
+
detector = AmharicLanguageDetector()
|
|
6
|
+
assert detector.is_amharic_text("á°áá") is True
|
|
7
|
+
assert detector.is_amharic_text("Hello") is False
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
class AmharicWordTokenizer:
|
|
4
|
+
"""
|
|
5
|
+
Tokenizer for Amharic text.
|
|
6
|
+
Handles Amharic punctuation, contractions, and parentheses/brackets.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Amharic starting quotes and punctuation
|
|
10
|
+
STARTING_QUOTES = [
|
|
11
|
+
(re.compile(r'^ÂŤ'), r'`'),
|
|
12
|
+
(re.compile(r'^Âť'), r"'"),
|
|
13
|
+
(re.compile(r'^"'), r''),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
# Amharic punctuation marks
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
PUNCTUATION= [
|
|
20
|
+
(re.compile(r'\.'), r' . '),
|
|
21
|
+
(re.compile(r'áĄ'), r' ᥠ'),
|
|
22
|
+
(re.compile(r'[!?]'), r' \g<0> '),
|
|
23
|
+
(re.compile(r'á˘'), r' ᢠ'),
|
|
24
|
+
(re.compile(r'áŁ'), r' ᣠ'),
|
|
25
|
+
(re.compile(r'á¤'), r' ᤠ'),
|
|
26
|
+
(re.compile(r'[-ââ]+'), r' \g<0> '),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Parentheses and brackets (replace with labels)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
PARENS_BRACKETS = [
|
|
33
|
+
(re.compile(r'([\(\)\[\]\{\}])'), r' \1 ')
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Amharic contractions and compound words
|
|
38
|
+
CONTRACTIONS = [
|
|
39
|
+
(re.compile(r'(?i)\b(á¨|á |á¨|á|ááľáĽ|áá|á áá)([^\s]+)\b'), r'\1 \2'), # Prepositions
|
|
40
|
+
(re.compile(r'(?i)\b(áĽá|á ááş|áĽáą|áĽáˇ)([^\s]+)\b'), r'\1 \2'), # Pronouns
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
def tokenize(self, text):
|
|
44
|
+
# Apply starting quotes
|
|
45
|
+
for regexp, substitution in self.STARTING_QUOTES:
|
|
46
|
+
text = regexp.sub(substitution, text)
|
|
47
|
+
|
|
48
|
+
# Handle punctuation
|
|
49
|
+
for regexp, substitution in self.PUNCTUATION:
|
|
50
|
+
text = regexp.sub(substitution, text)
|
|
51
|
+
|
|
52
|
+
# Replace parentheses/brackets
|
|
53
|
+
for regexp, substitution in self.PARENS_BRACKETS:
|
|
54
|
+
text = regexp.sub(substitution, text)
|
|
55
|
+
|
|
56
|
+
# Split contractions and compound words
|
|
57
|
+
for regexp, substitution in self.CONTRACTIONS:
|
|
58
|
+
text = regexp.sub(substitution, text)
|
|
59
|
+
|
|
60
|
+
# Split on whitespace and return tokens
|
|
61
|
+
tokens = text.strip().split()
|
|
62
|
+
return tokens
|
|
63
|
+
|
|
64
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class AmharicLanguageDetector:
|
|
2
|
+
def is_amharic_char(char: str) -> bool:
|
|
3
|
+
"""Check if a character is in the Amharic Unicode block."""
|
|
4
|
+
# Amharic Unicode range: U+1200 to U+137F
|
|
5
|
+
return '\u1200' <= char <= '\u137F'
|
|
6
|
+
|
|
7
|
+
def is_amharic_text(text: str, threshold: float = 0.7) -> bool:
|
|
8
|
+
"""
|
|
9
|
+
Check if text is primarily Amharic.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
text: Input text to check
|
|
13
|
+
threshold: Minimum proportion of Amharic characters to consider as Amharic text
|
|
14
|
+
"""
|
|
15
|
+
if not text:
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
amharic_count = sum(1 for char in text if AmharicLanguageDetector.is_amharic_char(char))
|
|
19
|
+
total_chars = len(text)
|
|
20
|
+
|
|
21
|
+
return (amharic_count / total_chars) >= threshold
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .data_augmenter import *
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from amharicNLP.resources.tokenizer import AmharicWordTokenizer
|
|
5
|
+
from amharicNLP.resources.cleaner import AmharicCleaner
|
|
6
|
+
from amharicNLP.resources.normalizer import AmharicNormalizer
|
|
7
|
+
from amharicNLP.resources.lemmatizer import AmharicLemmatizer
|
|
8
|
+
from amharicNLP.resources.stemmer import AmharicStemmer
|
|
9
|
+
# Initialize class-based components
|
|
10
|
+
tokenizer = AmharicWordTokenizer()
|
|
11
|
+
cleaner = AmharicCleaner()
|
|
12
|
+
|
|
13
|
+
SYNONYMS = {
|
|
14
|
+
"áĽáá´áľ": ["áĽáá°áá", "á áá á áááľ"],
|
|
15
|
+
"áá
": ["áá˝", "áá˝á"],
|
|
16
|
+
"á°áľ": ["áá°áľ", "á°áľáł"],
|
|
17
|
+
"áĽááá": ["á á°á°á", "á°áľ á áá"]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def random_deletion(words, p=0.2):
|
|
21
|
+
if len(words) < 2:
|
|
22
|
+
return words
|
|
23
|
+
return [w for w in words if random.random() > p] or [random.choice(words)]
|
|
24
|
+
|
|
25
|
+
def synonym_replacement(words, n=1):
|
|
26
|
+
new_words = words.copy()
|
|
27
|
+
indices = [i for i, w in enumerate(new_words) if w in SYNONYMS]
|
|
28
|
+
random.shuffle(indices)
|
|
29
|
+
|
|
30
|
+
for i in indices[:min(n, len(indices))]:
|
|
31
|
+
new_words[i] = random.choice(SYNONYMS[new_words[i]])
|
|
32
|
+
return new_words
|
|
33
|
+
|
|
34
|
+
def random_swap(words, n=1):
|
|
35
|
+
new_words = words.copy()
|
|
36
|
+
for _ in range(min(n, len(words) // 2)):
|
|
37
|
+
idx1, idx2 = random.sample(range(len(new_words)), 2)
|
|
38
|
+
new_words[idx1], new_words[idx2] = new_words[idx2], new_words[idx1]
|
|
39
|
+
return new_words
|
|
40
|
+
|
|
41
|
+
def augment_sentence(sentence, aug_prob=0.3):
|
|
42
|
+
words = tokenizer.tokenize(sentence)
|
|
43
|
+
if random.random() < aug_prob:
|
|
44
|
+
words = random_deletion(words)
|
|
45
|
+
if random.random() < aug_prob:
|
|
46
|
+
words = synonym_replacement(words, n=1)
|
|
47
|
+
if random.random() < aug_prob:
|
|
48
|
+
words = random_swap(words)
|
|
49
|
+
return tokenizer.detokenize(words)
|
|
50
|
+
|
|
51
|
+
# Preserving your original example usage
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
sentence = "áĽáá´áľ áá
á°áľ áĽááá"
|
|
54
|
+
print("Original:", sentence)
|
|
55
|
+
|
|
56
|
+
words = tokenizer.tokenize(sentence)
|
|
57
|
+
|
|
58
|
+
print("Random Deletion:", tokenizer.detokenize(random_deletion(words, p=0.3)))
|
|
59
|
+
print("Synonym Replacement:", tokenizer.detokenize(synonym_replacement(words, n=2)))
|
|
60
|
+
print("Random Swap:", tokenizer.detokenize(random_swap(words, n=2)))
|
|
61
|
+
print("Remove Diacritics:", cleaner.remove_diacritics(sentence))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from ..resources.lemmatizer import AmharicLemmatizer
|
|
4
|
+
|
|
5
|
+
# Load WordNet data
|
|
6
|
+
_wordnet_path = Path(__file__).parent / "amh_wordnet.json"
|
|
7
|
+
with open(_wordnet_path, encoding='utf-8') as f:
|
|
8
|
+
wordnet_data = json.load(f)
|
|
9
|
+
|
|
10
|
+
# Initialize lemmatizer (without stemmer)
|
|
11
|
+
lemmatizer = AmharicLemmatizer(wordnet_data)
|
|
12
|
+
|
|
13
|
+
# Make available at package level
|
|
14
|
+
__all__ = ['wordnet_data', 'lemmatizer']
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
# Automatically find the file relative to this script
|
|
5
|
+
this_dir = os.path.dirname(__file__)
|
|
6
|
+
wordnet_path = os.path.join(this_dir, "am_wordnet.json")
|
|
7
|
+
|
|
8
|
+
def load_amharic_wordnet():
|
|
9
|
+
with open(wordnet_path, encoding="utf-8") as f:
|
|
10
|
+
return json.load(f)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.py
|
|
6
|
+
amharicNLP/__init__.py
|
|
7
|
+
amharicNLP.egg-info/PKG-INFO
|
|
8
|
+
amharicNLP.egg-info/SOURCES.txt
|
|
9
|
+
amharicNLP.egg-info/dependency_links.txt
|
|
10
|
+
amharicNLP.egg-info/top_level.txt
|
|
11
|
+
amharicNLP/example/__init__.py
|
|
12
|
+
amharicNLP/example/test.py
|
|
13
|
+
amharicNLP/resources/__init__.py
|
|
14
|
+
amharicNLP/resources/cleaner.py
|
|
15
|
+
amharicNLP/resources/lemmatizer.py
|
|
16
|
+
amharicNLP/resources/normalizer.py
|
|
17
|
+
amharicNLP/resources/stemmer.py
|
|
18
|
+
amharicNLP/resources/stopwrod.py
|
|
19
|
+
amharicNLP/resources/tokenizer.py
|
|
20
|
+
amharicNLP/resources/utils.py
|
|
21
|
+
amharicNLP/resources/tests/__init__.py
|
|
22
|
+
amharicNLP/resources/tests/conftest.py
|
|
23
|
+
amharicNLP/resources/tests/test_cleaner.py
|
|
24
|
+
amharicNLP/resources/tests/test_lemmatizer.py
|
|
25
|
+
amharicNLP/resources/tests/test_normalizer.py
|
|
26
|
+
amharicNLP/resources/tests/test_utils.py
|
|
27
|
+
amharicNLP/utilities/__init__.py
|
|
28
|
+
amharicNLP/utilities/data_augmenter.py
|
|
29
|
+
amharicNLP/wordnet/__init__.py
|
|
30
|
+
amharicNLP/wordnet/loader.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
amharicNLP
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "0.1.2"
|