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.
Files changed (32) hide show
  1. amharicnlp-0.0.0/LICENSE +1 -0
  2. amharicnlp-0.0.0/MANIFEST.in +0 -0
  3. amharicnlp-0.0.0/PKG-INFO +5 -0
  4. amharicnlp-0.0.0/README.md +22 -0
  5. amharicnlp-0.0.0/amharicNLP/__init__.py +4 -0
  6. amharicnlp-0.0.0/amharicNLP/example/__init__.py +1 -0
  7. amharicnlp-0.0.0/amharicNLP/example/test.py +54 -0
  8. amharicnlp-0.0.0/amharicNLP/resources/__init__.py +9 -0
  9. amharicnlp-0.0.0/amharicNLP/resources/cleaner.py +58 -0
  10. amharicnlp-0.0.0/amharicNLP/resources/lemmatizer.py +84 -0
  11. amharicnlp-0.0.0/amharicNLP/resources/normalizer.py +300 -0
  12. amharicnlp-0.0.0/amharicNLP/resources/stemmer.py +85 -0
  13. amharicnlp-0.0.0/amharicNLP/resources/stopwrod.py +38 -0
  14. amharicnlp-0.0.0/amharicNLP/resources/tests/__init__.py +0 -0
  15. amharicnlp-0.0.0/amharicNLP/resources/tests/conftest.py +14 -0
  16. amharicnlp-0.0.0/amharicNLP/resources/tests/test_cleaner.py +14 -0
  17. amharicnlp-0.0.0/amharicNLP/resources/tests/test_lemmatizer.py +0 -0
  18. amharicnlp-0.0.0/amharicNLP/resources/tests/test_normalizer.py +6 -0
  19. amharicnlp-0.0.0/amharicNLP/resources/tests/test_utils.py +7 -0
  20. amharicnlp-0.0.0/amharicNLP/resources/tokenizer.py +64 -0
  21. amharicnlp-0.0.0/amharicNLP/resources/utils.py +21 -0
  22. amharicnlp-0.0.0/amharicNLP/utilities/__init__.py +1 -0
  23. amharicnlp-0.0.0/amharicNLP/utilities/data_augmenter.py +61 -0
  24. amharicnlp-0.0.0/amharicNLP/wordnet/__init__.py +14 -0
  25. amharicnlp-0.0.0/amharicNLP/wordnet/loader.py +10 -0
  26. amharicnlp-0.0.0/amharicNLP.egg-info/PKG-INFO +5 -0
  27. amharicnlp-0.0.0/amharicNLP.egg-info/SOURCES.txt +30 -0
  28. amharicnlp-0.0.0/amharicNLP.egg-info/dependency_links.txt +1 -0
  29. amharicnlp-0.0.0/amharicNLP.egg-info/top_level.txt +1 -0
  30. amharicnlp-0.0.0/pyproject.toml +1 -0
  31. amharicnlp-0.0.0/setup.cfg +4 -0
  32. amharicnlp-0.0.0/setup.py +5 -0
@@ -0,0 +1 @@
1
+ MIT License
File without changes
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: amharicNLP
3
+ Version: 0.0.0
4
+ License-File: LICENSE
5
+ Dynamic: license-file
@@ -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,4 @@
1
+ from .resources import *
2
+ from .example import *
3
+ from .wordnet import *
4
+ from .utilities import *
@@ -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)
@@ -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"
@@ -0,0 +1,6 @@
1
+ from amharicNLP .resources.normalizer import AmharicNormalizer
2
+
3
+ class TestAmharicNormalizer:
4
+ def test_normalize(self):
5
+ normalizer = AmharicNormalizer()
6
+ assert normalizer.normalize("ኣብዚ") == "አብዚ" # Example normalization
@@ -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,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: amharicNLP
3
+ Version: 0.0.0
4
+ License-File: LICENSE
5
+ Dynamic: license-file
@@ -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
+ amharicNLP
@@ -0,0 +1 @@
1
+ version = "0.1.2"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ # setup.py
2
+ from setuptools import setup
3
+
4
+ if __name__ == "__main__":
5
+ setup()