amharic-segmenter 0.1.0__py3-none-any.whl
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.
- amharic_segmenter/__init__.py +40 -0
- amharic_segmenter/__main__.py +3 -0
- amharic_segmenter/_version.py +1 -0
- amharic_segmenter/charset.py +69 -0
- amharic_segmenter/cli.py +71 -0
- amharic_segmenter/data/irregular_plurals.tsv +43 -0
- amharic_segmenter/data/lexicon.txt +110 -0
- amharic_segmenter/morphology.py +182 -0
- amharic_segmenter/normalize.py +29 -0
- amharic_segmenter/py.typed +0 -0
- amharic_segmenter/tokenizer.py +309 -0
- amharic_segmenter-0.1.0.dist-info/METADATA +340 -0
- amharic_segmenter-0.1.0.dist-info/RECORD +16 -0
- amharic_segmenter-0.1.0.dist-info/WHEEL +4 -0
- amharic_segmenter-0.1.0.dist-info/entry_points.txt +2 -0
- amharic_segmenter-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""amharic-segmenter: sentence, word and morpheme tokenization for Amharic (አማርኛ)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from ._version import __version__
|
|
5
|
+
from .morphology import Analysis
|
|
6
|
+
from .normalize import normalize_homophones
|
|
7
|
+
from .tokenizer import (
|
|
8
|
+
AmharicStemmer,
|
|
9
|
+
AmharicTokenizer,
|
|
10
|
+
Token,
|
|
11
|
+
analyze,
|
|
12
|
+
lemma,
|
|
13
|
+
sent_tokenize,
|
|
14
|
+
sentence_tokenize,
|
|
15
|
+
split_morphemes,
|
|
16
|
+
stem,
|
|
17
|
+
stem_words,
|
|
18
|
+
tokenize,
|
|
19
|
+
tokenize_spans,
|
|
20
|
+
word_tokenize,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"AmharicStemmer",
|
|
25
|
+
"AmharicTokenizer",
|
|
26
|
+
"Analysis",
|
|
27
|
+
"Token",
|
|
28
|
+
"__version__",
|
|
29
|
+
"analyze",
|
|
30
|
+
"lemma",
|
|
31
|
+
"normalize_homophones",
|
|
32
|
+
"sent_tokenize",
|
|
33
|
+
"sentence_tokenize",
|
|
34
|
+
"split_morphemes",
|
|
35
|
+
"stem",
|
|
36
|
+
"stem_words",
|
|
37
|
+
"tokenize",
|
|
38
|
+
"tokenize_spans",
|
|
39
|
+
"word_tokenize",
|
|
40
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Low-level helpers for the Ethiopic (Fidel) script.
|
|
2
|
+
|
|
3
|
+
Each Ethiopic consonant occupies a block of 8 consecutive code points, one per
|
|
4
|
+
vowel *order*. The first seven are the classic orders used in Amharic
|
|
5
|
+
grammar books (the names below follow the traditional Ge'ez terminology):
|
|
6
|
+
|
|
7
|
+
0 ግዕዝ (ä) 1 ካዕብ (u) 2 ሣልስ (i) 3 ራብዕ (a)
|
|
8
|
+
4 ሃምስ (e) 5 ሳድስ (ə / no vowel) 6 ሳብዕ (o)
|
|
9
|
+
|
|
10
|
+
The eighth slot of a block is a labialised form (e.g. ሏ) and is ignored here.
|
|
11
|
+
A few blocks (e.g. ቈ, ኰ, ጐ) are irregular in Unicode and are deliberately
|
|
12
|
+
left out, because they never occur as word-final letters in ordinary text.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
GEEZ, KAEB, SALIS, RABI, HAMIS, SADIS, SABI = range(7)
|
|
17
|
+
|
|
18
|
+
ORDER_NAMES = ("ግዕዝ", "ካዕብ", "ሣልስ", "ራብዕ", "ሃምስ", "ሳድስ", "ሳብዕ")
|
|
19
|
+
|
|
20
|
+
_FIRST = 0x1200
|
|
21
|
+
_LAST_REGULAR = 0x1357
|
|
22
|
+
_IRREGULAR = (
|
|
23
|
+
(0x1248, 0x124F),
|
|
24
|
+
(0x1258, 0x125F),
|
|
25
|
+
(0x1288, 0x128F),
|
|
26
|
+
(0x12B0, 0x12B7),
|
|
27
|
+
(0x12C0, 0x12C7),
|
|
28
|
+
(0x1310, 0x1317),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_ethiopic_letter(ch: str) -> bool:
|
|
33
|
+
"""True for a character in the Ethiopic syllable range."""
|
|
34
|
+
return _FIRST <= ord(ch) <= 0x135A
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_regular(cp: int) -> bool:
|
|
38
|
+
if not (_FIRST <= cp <= _LAST_REGULAR):
|
|
39
|
+
return False
|
|
40
|
+
return not any(lo <= cp <= hi for lo, hi in _IRREGULAR)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def fidel_order(ch: str) -> int | None:
|
|
44
|
+
"""Return the vowel order (0-6) of a fidel, or ``None`` if not applicable."""
|
|
45
|
+
cp = ord(ch)
|
|
46
|
+
if not _is_regular(cp):
|
|
47
|
+
return None
|
|
48
|
+
order = (cp - _FIRST) % 8
|
|
49
|
+
return order if order <= SABI else None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_wa_form(ch: str) -> bool:
|
|
53
|
+
"""True for the labialised 8th form of a letter (ሟ, ሏ, ጓ …).
|
|
54
|
+
|
|
55
|
+
It is how the feminine ``ዋ`` is written when it fuses into a consonant,
|
|
56
|
+
e.g. ሐኪም + ዋ -> ሐኪሟ.
|
|
57
|
+
"""
|
|
58
|
+
cp = ord(ch)
|
|
59
|
+
return _is_regular(cp) and (cp - _FIRST) % 8 == 7
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def with_order(ch: str, order: int) -> str:
|
|
63
|
+
"""Return the fidel with the same consonant as ``ch`` in another vowel order."""
|
|
64
|
+
current = fidel_order(ch)
|
|
65
|
+
if current is None:
|
|
66
|
+
raise ValueError(f"{ch!r} is not a regular Ethiopic syllable")
|
|
67
|
+
if not 0 <= order <= SABI:
|
|
68
|
+
raise ValueError("order must be between 0 and 6")
|
|
69
|
+
return chr(ord(ch) - current + order)
|
amharic_segmenter/cli.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Command line interface: ``amharic-segmenter`` / ``python -m amharic_segmenter``."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from ._version import __version__
|
|
9
|
+
from .tokenizer import AmharicTokenizer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
13
|
+
p = argparse.ArgumentParser(
|
|
14
|
+
prog="amharic-segmenter",
|
|
15
|
+
description="Tokenize Amharic text into sentences, words or morphemes.",
|
|
16
|
+
)
|
|
17
|
+
p.add_argument("text", nargs="?", help="text to tokenize (reads stdin when omitted)")
|
|
18
|
+
p.add_argument("-s", "--sentences", action="store_true", help="split into sentences instead of words")
|
|
19
|
+
p.add_argument(
|
|
20
|
+
"-m", "--morphemes", action="store_true", help="split words into prefixes, stems and suffixes"
|
|
21
|
+
)
|
|
22
|
+
p.add_argument("--stem", action="store_true", help="print the stem of every word")
|
|
23
|
+
p.add_argument("--mark-affixes", action="store_true", help="write prefixes as 'በ+' and suffixes as '+ን'")
|
|
24
|
+
p.add_argument("--strategy", choices=["lexicon", "rules", "none"], default="lexicon",
|
|
25
|
+
help="morpheme splitting strategy (default: lexicon)")
|
|
26
|
+
p.add_argument("--lexicon", metavar="FILE", help="UTF-8 file with extra stems, one per line")
|
|
27
|
+
p.add_argument(
|
|
28
|
+
"--normalize", action="store_true", help="collapse homophone letters (ሐ->ሀ, ሠ->ሰ, ዐ->አ, ፀ->ጸ)"
|
|
29
|
+
)
|
|
30
|
+
p.add_argument("--no-punct", action="store_true", help="drop punctuation tokens")
|
|
31
|
+
p.add_argument("--json", action="store_true", help="print the result as JSON")
|
|
32
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
33
|
+
return p
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(argv: list[str] | None = None) -> int:
|
|
37
|
+
args = build_parser().parse_args(argv)
|
|
38
|
+
for stream in (sys.stdout, sys.stdin):
|
|
39
|
+
if hasattr(stream, "reconfigure"):
|
|
40
|
+
stream.reconfigure(encoding="utf-8") # type: ignore[union-attr]
|
|
41
|
+
|
|
42
|
+
text = args.text if args.text is not None else sys.stdin.read()
|
|
43
|
+
kwargs = dict(strategy=args.strategy, normalize=args.normalize)
|
|
44
|
+
tok = (AmharicTokenizer.from_file(args.lexicon, **kwargs) if args.lexicon
|
|
45
|
+
else AmharicTokenizer(**kwargs))
|
|
46
|
+
|
|
47
|
+
if args.stem:
|
|
48
|
+
result = [tok.stem(t.text) for t in tok.tokenize_spans(text, include_punct=not args.no_punct)
|
|
49
|
+
if t.kind != "punct" or not args.no_punct]
|
|
50
|
+
sep = " "
|
|
51
|
+
elif args.sentences:
|
|
52
|
+
result = tok.sentence_tokenize(text)
|
|
53
|
+
sep = "\n"
|
|
54
|
+
else:
|
|
55
|
+
result = tok.tokenize(
|
|
56
|
+
text,
|
|
57
|
+
morphemes=args.morphemes,
|
|
58
|
+
include_punct=not args.no_punct,
|
|
59
|
+
mark_affixes=args.mark_affixes,
|
|
60
|
+
)
|
|
61
|
+
sep = " "
|
|
62
|
+
|
|
63
|
+
if args.json:
|
|
64
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
65
|
+
else:
|
|
66
|
+
print(sep.join(result))
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__": # pragma: no cover
|
|
71
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Irregular plurals: singular<TAB>plural ('#' = comment)
|
|
2
|
+
#
|
|
3
|
+
# Transcribed from photographed pages of አማርኛ ሠዋሰው (1963 E.C.): the Geez-origin
|
|
4
|
+
# plurals in section 2.2 (pp. 42-44) and the reduplicated adjective plurals in
|
|
5
|
+
# section 2.3.4 (pp. 47-48). Both forms are added to the lexicon, so they can take
|
|
6
|
+
# suffixes (መጻሕፍትን) and the regular plural can be added on top (ቃላቶች).
|
|
7
|
+
# Only entries that are clearly legible in the photos are listed - please review.
|
|
8
|
+
|
|
9
|
+
# --- Geez-origin plurals (section 2.2) ---
|
|
10
|
+
ካህን ካህናት
|
|
11
|
+
ጳጳስ ጳጳሳት
|
|
12
|
+
ቃል ቃላት
|
|
13
|
+
ሐዋርያ ሐዋርያት
|
|
14
|
+
ንጉሥ ነገሥታት
|
|
15
|
+
ነቢይ ነቢያት
|
|
16
|
+
መጽሐፍ መጻሕፍት
|
|
17
|
+
መምህር መምህራን
|
|
18
|
+
መስፍን መሳፍንት
|
|
19
|
+
ደብር አድባራት
|
|
20
|
+
ቄስ ቀሳውስት
|
|
21
|
+
መልአክ መላእክት
|
|
22
|
+
ሊቅ ሊቃውንት
|
|
23
|
+
ቤት አብያት
|
|
24
|
+
ርእስ አርእስት
|
|
25
|
+
ሕዝብ አሕዛብ
|
|
26
|
+
ዐምድ አዕማድ
|
|
27
|
+
ገዳም ገዳማት
|
|
28
|
+
አንቀጽ አናቅጽ
|
|
29
|
+
ድንግል ደናግል
|
|
30
|
+
አንበሳ አናብስት
|
|
31
|
+
ተዋናይ ተዋናያን
|
|
32
|
+
ማኅበር ማኅበራት
|
|
33
|
+
ኢትዮጵያ ኢትዮጵያውያን
|
|
34
|
+
|
|
35
|
+
# --- adjectives with reduplicated plurals (section 2.3.4) ---
|
|
36
|
+
ደግ ደጋግ
|
|
37
|
+
ጥቁር ጥቋቁር
|
|
38
|
+
ረጅም ረጃጅም
|
|
39
|
+
ትልቅ ትልልቅ
|
|
40
|
+
ትንሽ ትንንሽ
|
|
41
|
+
አጭር አጫጭር
|
|
42
|
+
ቀይ ቀያይ
|
|
43
|
+
ነጭ ነጫጭ
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Seed lexicon for amharic-segmenter (UTF-8, one word per line, '#' = comment).
|
|
2
|
+
#
|
|
3
|
+
# These are stems that may take prefixes/suffixes. A split such as ተማሪዎች ->
|
|
4
|
+
# ተማሪ + ዎች is only accepted when the stem is listed here (or when the
|
|
5
|
+
# opt-in "rules" strategy is used). The list starts with the nouns, names and
|
|
6
|
+
# function words that appear as examples in አማርኛ ሠዋሰው (1963 E.C.); grow it
|
|
7
|
+
# with a pull request or pass your own words to AmharicTokenizer(lexicon=...).
|
|
8
|
+
|
|
9
|
+
# names
|
|
10
|
+
ከበደ
|
|
11
|
+
አበበ
|
|
12
|
+
ተፈራ
|
|
13
|
+
|
|
14
|
+
# people and relations
|
|
15
|
+
ሰው
|
|
16
|
+
ልጅ
|
|
17
|
+
ተማሪ
|
|
18
|
+
አስተማሪ
|
|
19
|
+
ሐኪም
|
|
20
|
+
ጸሐፊ
|
|
21
|
+
ሠራተኛ
|
|
22
|
+
ዘበኛ
|
|
23
|
+
ገበሬ
|
|
24
|
+
ወንድም
|
|
25
|
+
አባት
|
|
26
|
+
እናት
|
|
27
|
+
ወንድ
|
|
28
|
+
ሴት
|
|
29
|
+
ሴትዮ
|
|
30
|
+
ባለቤት
|
|
31
|
+
|
|
32
|
+
# animals
|
|
33
|
+
ፈረስ
|
|
34
|
+
በሬ
|
|
35
|
+
በቅሎ
|
|
36
|
+
ላም
|
|
37
|
+
ዶሮ
|
|
38
|
+
|
|
39
|
+
# things and places
|
|
40
|
+
ቤት
|
|
41
|
+
ጓዳ
|
|
42
|
+
ጣራ
|
|
43
|
+
መጽሐፍ
|
|
44
|
+
ወረቀት
|
|
45
|
+
ብዕር
|
|
46
|
+
በር
|
|
47
|
+
ወንበር
|
|
48
|
+
ጠረጴዛ
|
|
49
|
+
እርሳስ
|
|
50
|
+
እንጀራ
|
|
51
|
+
ልብስ
|
|
52
|
+
ድንጋይ
|
|
53
|
+
መሰንቆ
|
|
54
|
+
ከተማ
|
|
55
|
+
አገር
|
|
56
|
+
ትምህርት
|
|
57
|
+
ጌታ
|
|
58
|
+
|
|
59
|
+
# adjectives and other stems used in examples
|
|
60
|
+
ችግር
|
|
61
|
+
ክፉ
|
|
62
|
+
ደግ
|
|
63
|
+
ብቻ
|
|
64
|
+
|
|
65
|
+
# pronouns and demonstratives (take ን, ና, ም)
|
|
66
|
+
እኔ
|
|
67
|
+
እኛ
|
|
68
|
+
አንተ
|
|
69
|
+
አንቺ
|
|
70
|
+
እሱ
|
|
71
|
+
እሷ
|
|
72
|
+
እርሱ
|
|
73
|
+
እርሷ
|
|
74
|
+
እነሱ
|
|
75
|
+
እናንተ
|
|
76
|
+
ይህ
|
|
77
|
+
ይህች
|
|
78
|
+
ያ
|
|
79
|
+
ያች
|
|
80
|
+
እነዚህ
|
|
81
|
+
እነዚያ
|
|
82
|
+
|
|
83
|
+
# common function words: never split, whatever suffix they happen to end with
|
|
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
|
+
ነገር
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Rule-based morphological segmentation for Amharic nouns and noun phrases.
|
|
2
|
+
|
|
3
|
+
The rules implemented here come from the grammar book *አማርኛ ሠዋሰው* (1963 E.C.):
|
|
4
|
+
prepositions that attach to the following word, the plural, the definite
|
|
5
|
+
article and possessive suffixes, the object marker ``ን`` and the conjunctions
|
|
6
|
+
``ና`` / ``ም``. See ``docs/GRAMMAR_RULES.md`` for the rule-by-rule mapping.
|
|
7
|
+
|
|
8
|
+
A candidate split is only accepted when the remaining *stem* is trustworthy:
|
|
9
|
+
either it is in the lexicon (default, high precision) or, in the opt-in
|
|
10
|
+
``"rules"`` strategy, it is at least ``min_stem`` letters long.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Iterator, Set
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from .charset import KAEB, RABI, SABI, SADIS, fidel_order, is_wa_form, with_order
|
|
19
|
+
|
|
20
|
+
# Prefixes written together with the word that follows them.
|
|
21
|
+
PREFIXES: tuple[str, ...] = ("ስለ", "በ", "ከ", "ለ", "የ")
|
|
22
|
+
MAX_PREFIXES = 2
|
|
23
|
+
|
|
24
|
+
# Suffix slots, from the stem outward: plural -> possessive/definite -> ን -> ና/ም
|
|
25
|
+
CONJUNCTIONS: tuple[str, ...] = ("ና", "ም")
|
|
26
|
+
OBJECT_MARKER = "ን"
|
|
27
|
+
POSSESSIVE_DEFINITE: tuple[str, ...] = ("ቸው", "ችሁ", "ችን", "ው", "ዋ", "ህ", "ሽ", "ዬ")
|
|
28
|
+
# Suffixes before which the stem's last letter surfaces in the 'a' order
|
|
29
|
+
# (አገር + ችሁ -> አገራችሁ, ተማሪዎች + ችን -> ተማሪዎቻችን).
|
|
30
|
+
_LINKING: tuple[str, ...] = ("ቸው", "ችሁ", "ችን")
|
|
31
|
+
|
|
32
|
+
# Prepositions fuse with pronouns that start with እ: በ + እኔ -> በኔ, ከ + እኛ -> ከኛ,
|
|
33
|
+
# እንደ + አንተ -> እንዳንተ.
|
|
34
|
+
_CONTRACTING_PRONOUNS: tuple[str, ...] = ("እኔ", "እኛ", "እሱ", "እሷ", "እነሱ", "እርሱ", "እርሷ", "እናንተ")
|
|
35
|
+
CONTRACTION_PREFIXES: tuple[str, ...] = ("በ", "ከ", "ለ", "የ", "እንደ")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_contractions() -> dict[str, tuple[str, str]]:
|
|
39
|
+
table = {}
|
|
40
|
+
for prefix in CONTRACTION_PREFIXES:
|
|
41
|
+
for pronoun in _CONTRACTING_PRONOUNS:
|
|
42
|
+
table[prefix + pronoun[1:]] = (prefix, pronoun)
|
|
43
|
+
for pronoun in ("አንተ", "አንቺ"):
|
|
44
|
+
table["እንዳ" + pronoun[1:]] = ("እንደ", pronoun)
|
|
45
|
+
return table
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
CONTRACTIONS = _build_contractions()
|
|
49
|
+
|
|
50
|
+
_ETHIOPIC_WORD = re.compile(r"^[\u1200-\u135A]+$")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Analysis:
|
|
55
|
+
"""Result of segmenting one word.
|
|
56
|
+
|
|
57
|
+
``morphemes`` are normalised morphs (underlying forms), so concatenating
|
|
58
|
+
them does not always reproduce the surface word (e.g. ልጆች -> ልጅ + ች).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
word: str
|
|
62
|
+
prefixes: tuple[str, ...] = ()
|
|
63
|
+
stem: str = ""
|
|
64
|
+
suffixes: tuple[str, ...] = ()
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def morphemes(self) -> list[str]:
|
|
68
|
+
return [*self.prefixes, self.stem, *self.suffixes]
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def is_split(self) -> bool:
|
|
72
|
+
return bool(self.prefixes or self.suffixes)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _opt(word: str, suffixes: tuple[str, ...]) -> Iterator[tuple[str, str | None]]:
|
|
76
|
+
yield word, None
|
|
77
|
+
for s in suffixes:
|
|
78
|
+
if len(word) > len(s) and word.endswith(s):
|
|
79
|
+
yield word[: -len(s)], s
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _opt_possessive(word: str) -> Iterator[tuple[str, str | None, bool]]:
|
|
83
|
+
"""Possessive / definite-article slot, including fused surface forms.
|
|
84
|
+
|
|
85
|
+
The third item is ``True`` for *fused* forms (ሐኪሟ, ልጁ), which only nouns have.
|
|
86
|
+
They are accepted for known stems only, so verbs such as አየሁ are never touched.
|
|
87
|
+
"""
|
|
88
|
+
yield word, None, False
|
|
89
|
+
for s in POSSESSIVE_DEFINITE:
|
|
90
|
+
if len(word) > len(s) and word.endswith(s):
|
|
91
|
+
rest = word[: -len(s)]
|
|
92
|
+
yield rest, s, False
|
|
93
|
+
if s in _LINKING and rest:
|
|
94
|
+
last = rest[-1]
|
|
95
|
+
if fidel_order(last) == RABI: # አገራችሁ -> አገር, ተማሪዎቻችን -> ተማሪዎች
|
|
96
|
+
yield rest[:-1] + with_order(last, SADIS), s, False
|
|
97
|
+
# feminine 'ዋ' fused into the last letter (labialised form): ሐኪሟ -> ሐኪም + ዋ
|
|
98
|
+
if len(word) >= 2 and is_wa_form(word[-1]):
|
|
99
|
+
yield word[:-1] + chr(ord(word[-1]) - 2), "ዋ", True
|
|
100
|
+
# masculine definite 'ኡ' fused into the last letter: ልጁ -> ልጅ, ተማሪዎቹ -> ተማሪዎች
|
|
101
|
+
if len(word) >= 2 and fidel_order(word[-1]) == KAEB:
|
|
102
|
+
yield word[:-1] + with_order(word[-1], SADIS), "ው", True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _opt_plural(word: str) -> Iterator[tuple[str, str | None]]:
|
|
106
|
+
"""Plural slot (the 1948 spelling convention described in the book)."""
|
|
107
|
+
yield word, None
|
|
108
|
+
if len(word) > 2 and word.endswith("ዎች"):
|
|
109
|
+
yield word[:-2], "ዎች"
|
|
110
|
+
if len(word) >= 2 and word.endswith("ች"):
|
|
111
|
+
stem0, prev = word[:-2], word[-2]
|
|
112
|
+
if fidel_order(prev) == SABI:
|
|
113
|
+
for order in (SADIS, RABI): # ሰዎች -> ሰው, ጌቶች -> ጌታ
|
|
114
|
+
yield stem0 + with_order(prev, order), "ች"
|
|
115
|
+
yield stem0 + prev, "ች" # በቅሎች -> በቅሎ
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _suffix_variants(word: str) -> Iterator[tuple[str, tuple[str, ...], bool]]:
|
|
119
|
+
for w1, conj in _opt(word, CONJUNCTIONS):
|
|
120
|
+
for w2, obj in _opt(w1, (OBJECT_MARKER,)):
|
|
121
|
+
for w3, pos, fused in _opt_possessive(w2):
|
|
122
|
+
for w4, plural in _opt_plural(w3):
|
|
123
|
+
yield w4, tuple(x for x in (plural, pos, obj, conj) if x), fused
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _prefix_variants(stem: str, depth: int = MAX_PREFIXES) -> Iterator[tuple[tuple[str, ...], str]]:
|
|
127
|
+
yield (), stem
|
|
128
|
+
if depth == 0:
|
|
129
|
+
return
|
|
130
|
+
for p in PREFIXES:
|
|
131
|
+
if len(stem) > len(p) and stem.startswith(p):
|
|
132
|
+
for rest_prefixes, rest in _prefix_variants(stem[len(p):], depth - 1):
|
|
133
|
+
yield (p, *rest_prefixes), rest
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def analyze_word(
|
|
137
|
+
word: str,
|
|
138
|
+
lexicon: Set[str],
|
|
139
|
+
strategy: str = "lexicon",
|
|
140
|
+
min_stem: int = 2,
|
|
141
|
+
) -> Analysis:
|
|
142
|
+
"""Segment ``word`` into prefixes, a stem and suffixes.
|
|
143
|
+
|
|
144
|
+
``strategy``:
|
|
145
|
+
* ``"lexicon"`` - accept a split only if the stem is a known word.
|
|
146
|
+
* ``"rules"`` - also accept any stem of at least ``min_stem`` letters
|
|
147
|
+
(higher recall, more false splits).
|
|
148
|
+
* ``"none"`` - never split.
|
|
149
|
+
"""
|
|
150
|
+
unsplit = Analysis(word, (), word, ())
|
|
151
|
+
if strategy == "none" or word in lexicon or not _ETHIOPIC_WORD.match(word):
|
|
152
|
+
return unsplit
|
|
153
|
+
|
|
154
|
+
# Ranking of candidate analyses:
|
|
155
|
+
# * a stem found in the lexicon always beats an unknown one; among those,
|
|
156
|
+
# fewer affixes and then the longer stem win (conservative);
|
|
157
|
+
# * for unknown stems ("rules" strategy) avoid guessing prefixes, then peel
|
|
158
|
+
# as many suffixes as possible (e.g. ቸው rather than only ው).
|
|
159
|
+
best: Analysis | None = None
|
|
160
|
+
best_key: tuple[int, ...] | None = None
|
|
161
|
+
for stem_candidate, suffixes, fused in _suffix_variants(word):
|
|
162
|
+
contraction = CONTRACTIONS.get(stem_candidate)
|
|
163
|
+
if contraction is not None and contraction[1] in lexicon:
|
|
164
|
+
prefix, pronoun = contraction
|
|
165
|
+
key = (0, 1 + len(suffixes), -len(pronoun))
|
|
166
|
+
if best_key is None or key < best_key:
|
|
167
|
+
best_key, best = key, Analysis(word, (prefix,), pronoun, suffixes)
|
|
168
|
+
for prefixes, stem in _prefix_variants(stem_candidate):
|
|
169
|
+
if not (prefixes or suffixes):
|
|
170
|
+
continue
|
|
171
|
+
known = stem in lexicon
|
|
172
|
+
if fused and not known:
|
|
173
|
+
continue # fused forms are only trusted for words in the lexicon
|
|
174
|
+
if not (known or (strategy == "rules" and len(stem) >= min_stem)):
|
|
175
|
+
continue
|
|
176
|
+
if known:
|
|
177
|
+
key: tuple[int, ...] = (0, len(prefixes) + len(suffixes), -len(stem))
|
|
178
|
+
else:
|
|
179
|
+
key = (1, len(prefixes), -len(suffixes), len(stem))
|
|
180
|
+
if best_key is None or key < best_key:
|
|
181
|
+
best_key, best = key, Analysis(word, prefixes, stem, suffixes)
|
|
182
|
+
return best or unsplit
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Optional orthographic normalisation for Amharic text.
|
|
2
|
+
|
|
3
|
+
Amharic has several groups of letters that are pronounced identically
|
|
4
|
+
(ሀ/ሐ/ኀ, ሰ/ሠ, አ/ዐ, ጸ/ፀ). Writers use them inconsistently, which fragments
|
|
5
|
+
vocabulary in NLP pipelines. :func:`normalize_homophones` collapses each
|
|
6
|
+
group to one representative while preserving the vowel order, and it never
|
|
7
|
+
changes the length of the text (so token offsets stay valid).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
# source block start -> representative block start (7 vowel orders each)
|
|
12
|
+
_BLOCKS = {
|
|
13
|
+
0x1210: 0x1200, # ሐ -> ሀ
|
|
14
|
+
0x1280: 0x1200, # ኀ -> ሀ
|
|
15
|
+
0x1220: 0x1230, # ሠ -> ሰ
|
|
16
|
+
0x12D0: 0x12A0, # ዐ -> አ
|
|
17
|
+
0x1340: 0x1338, # ፀ -> ጸ
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_TABLE = {
|
|
21
|
+
src + i: dst + i
|
|
22
|
+
for src, dst in _BLOCKS.items()
|
|
23
|
+
for i in range(7)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def normalize_homophones(text: str) -> str:
|
|
28
|
+
"""Collapse homophone letter families to a single representative."""
|
|
29
|
+
return text.translate(_TABLE)
|
|
File without changes
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Word, sentence and morpheme tokenization for Amharic."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Iterable, Iterator
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from importlib import resources
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import NamedTuple
|
|
10
|
+
|
|
11
|
+
from .morphology import Analysis, analyze_word
|
|
12
|
+
from .normalize import normalize_homophones
|
|
13
|
+
|
|
14
|
+
_STRATEGIES = ("lexicon", "rules", "none")
|
|
15
|
+
|
|
16
|
+
# One alternation, tried in order. Every non-space character belongs to
|
|
17
|
+
# exactly one token, so nothing is ever silently dropped.
|
|
18
|
+
_TOKEN_RE = re.compile(
|
|
19
|
+
r"(?P<word>[\u1200-\u135A\u135D-\u135F\u1380-\u138F]+)"
|
|
20
|
+
r"|(?P<ethnum>[\u1369-\u137C]+)"
|
|
21
|
+
r"|(?P<latin>[A-Za-z\u00C0-\u024F]+(?:['\u2019\-][A-Za-z\u00C0-\u024F]+)*)"
|
|
22
|
+
r"|(?P<number>\d+(?:[.,]\d+)*)"
|
|
23
|
+
r"|(?P<punct>\u1361\u1361|[\u1360-\u1368]|[^\w\s])"
|
|
24
|
+
r"|(?P<other>\S)"
|
|
25
|
+
)
|
|
26
|
+
_KINDS = {
|
|
27
|
+
"word": "word",
|
|
28
|
+
"ethnum": "number",
|
|
29
|
+
"latin": "latin",
|
|
30
|
+
"number": "number",
|
|
31
|
+
"punct": "punct",
|
|
32
|
+
"other": "other",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Sentence terminators: ። ፧ ፨ ! ? and the old-style double word-separator ፡፡
|
|
36
|
+
_TERMINATOR_RE = re.compile(r"(?:\u1361\u1361|[\u1362\u1367\u1368!?]+)[\u00BB\u201D\"')\]]*")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Token(NamedTuple):
|
|
40
|
+
"""A token with its character offsets in the (normalised) input text."""
|
|
41
|
+
|
|
42
|
+
text: str
|
|
43
|
+
start: int
|
|
44
|
+
end: int
|
|
45
|
+
kind: str # "word", "latin", "number", "punct" or "other"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _parse_lexicon(text: str) -> Iterator[str]:
|
|
49
|
+
for line in text.splitlines():
|
|
50
|
+
line = line.strip()
|
|
51
|
+
if line and not line.startswith("#"):
|
|
52
|
+
yield line
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@lru_cache(maxsize=1)
|
|
56
|
+
def load_irregular_plurals() -> tuple[tuple[str, str], ...]:
|
|
57
|
+
"""Load (singular, plural) pairs of irregular plurals shipped with the package."""
|
|
58
|
+
resource = resources.files("amharic_segmenter") / "data" / "irregular_plurals.tsv"
|
|
59
|
+
pairs = []
|
|
60
|
+
for line in _parse_lexicon(resource.read_text(encoding="utf-8")):
|
|
61
|
+
parts = line.split("\t")
|
|
62
|
+
if len(parts) == 2:
|
|
63
|
+
pairs.append((parts[0].strip(), parts[1].strip()))
|
|
64
|
+
return tuple(pairs)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@lru_cache(maxsize=1)
|
|
68
|
+
def load_default_lexicon() -> frozenset:
|
|
69
|
+
"""Load the seed lexicon (plus irregular plural forms) shipped with the package."""
|
|
70
|
+
resource = resources.files("amharic_segmenter") / "data" / "lexicon.txt"
|
|
71
|
+
words = set(_parse_lexicon(resource.read_text(encoding="utf-8")))
|
|
72
|
+
for singular, plural in load_irregular_plurals():
|
|
73
|
+
words.update((singular, plural))
|
|
74
|
+
return frozenset(words)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AmharicTokenizer:
|
|
78
|
+
"""Configurable Amharic tokenizer.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
lexicon:
|
|
83
|
+
Extra stems (an iterable of words) that are allowed to be split off.
|
|
84
|
+
default_lexicon:
|
|
85
|
+
Include the seed lexicon that ships with the package.
|
|
86
|
+
strategy:
|
|
87
|
+
``"lexicon"`` (default, high precision), ``"rules"`` (higher recall) or
|
|
88
|
+
``"none"`` (never split morphemes).
|
|
89
|
+
normalize:
|
|
90
|
+
Collapse homophone letters (ሐ->ሀ, ሠ->ሰ, ዐ->አ, ፀ->ጸ) before tokenizing.
|
|
91
|
+
min_stem:
|
|
92
|
+
Minimum stem length (in letters) for the ``"rules"`` strategy.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
lexicon: Iterable[str] | None = None,
|
|
98
|
+
*,
|
|
99
|
+
default_lexicon: bool = True,
|
|
100
|
+
strategy: str = "lexicon",
|
|
101
|
+
normalize: bool = False,
|
|
102
|
+
min_stem: int = 2,
|
|
103
|
+
) -> None:
|
|
104
|
+
if strategy not in _STRATEGIES:
|
|
105
|
+
raise ValueError(f"strategy must be one of {_STRATEGIES}, got {strategy!r}")
|
|
106
|
+
self.strategy = strategy
|
|
107
|
+
self.normalize = normalize
|
|
108
|
+
self.min_stem = min_stem
|
|
109
|
+
words = set(load_default_lexicon()) if default_lexicon else set()
|
|
110
|
+
if lexicon is not None:
|
|
111
|
+
words.update(w.strip() for w in lexicon if w.strip())
|
|
112
|
+
self._lexicon = frozenset(self._prep(w) for w in words)
|
|
113
|
+
self._irregular: dict[str, str] = (
|
|
114
|
+
{self._prep(pl): self._prep(sg) for sg, pl in load_irregular_plurals()}
|
|
115
|
+
if default_lexicon else {}
|
|
116
|
+
)
|
|
117
|
+
self._cache: dict[str, Analysis] = {}
|
|
118
|
+
self._rules_cache: dict[str, Analysis] = {}
|
|
119
|
+
|
|
120
|
+
# -- helpers ---------------------------------------------------------
|
|
121
|
+
def _prep(self, text: str) -> str:
|
|
122
|
+
return normalize_homophones(text) if self.normalize else text
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def from_file(cls, path: str | Path, **kwargs) -> AmharicTokenizer:
|
|
126
|
+
"""Build a tokenizer with extra stems read from a UTF-8 file."""
|
|
127
|
+
words = list(_parse_lexicon(Path(path).read_text(encoding="utf-8")))
|
|
128
|
+
return cls(lexicon=words, **kwargs)
|
|
129
|
+
|
|
130
|
+
def add_words(self, words: Iterable[str]) -> None:
|
|
131
|
+
"""Add stems to the lexicon of this tokenizer."""
|
|
132
|
+
self._lexicon = self._lexicon | {self._prep(w.strip()) for w in words if w.strip()}
|
|
133
|
+
self._cache.clear()
|
|
134
|
+
self._rules_cache.clear()
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def lexicon(self) -> frozenset:
|
|
138
|
+
return self._lexicon
|
|
139
|
+
|
|
140
|
+
# -- morphology ------------------------------------------------------
|
|
141
|
+
def analyze(self, word: str) -> Analysis:
|
|
142
|
+
"""Segment one word into prefixes, stem and suffixes."""
|
|
143
|
+
word = self._prep(word)
|
|
144
|
+
cached = self._cache.get(word)
|
|
145
|
+
if cached is None:
|
|
146
|
+
cached = analyze_word(word, self._lexicon, self.strategy, self.min_stem)
|
|
147
|
+
self._cache[word] = cached
|
|
148
|
+
return cached
|
|
149
|
+
|
|
150
|
+
def split_morphemes(self, word: str) -> list[str]:
|
|
151
|
+
"""Return the morphemes of one word, e.g. ``['ለ', 'ተማሪ', 'ዎች']``."""
|
|
152
|
+
return self.analyze(word).morphemes
|
|
153
|
+
|
|
154
|
+
def stem(self, word: str, *, aggressive: bool = True) -> str:
|
|
155
|
+
"""Strip prefixes and suffixes and return the stem (like ``PorterStemmer.stem``).
|
|
156
|
+
|
|
157
|
+
Known words are segmented with the lexicon. With ``aggressive=True`` (the
|
|
158
|
+
default) a word that is not in the lexicon still loses its suffixes using
|
|
159
|
+
the grammar rules alone, so the stemmer works on any noun, as English
|
|
160
|
+
stemmers do. Use ``aggressive=False`` to leave unknown words untouched.
|
|
161
|
+
"""
|
|
162
|
+
word = self._prep(word)
|
|
163
|
+
analysis = self.analyze(word)
|
|
164
|
+
if not analysis.is_split and aggressive and self.strategy == "lexicon":
|
|
165
|
+
analysis = self._rules_cache.get(word) # type: ignore[assignment]
|
|
166
|
+
if analysis is None:
|
|
167
|
+
analysis = analyze_word(word, self._lexicon, "rules", self.min_stem)
|
|
168
|
+
self._rules_cache[word] = analysis
|
|
169
|
+
return analysis.stem
|
|
170
|
+
|
|
171
|
+
def lemma(self, word: str) -> str:
|
|
172
|
+
"""Dictionary form of the stem: ``መጻሕፍትን -> መጽሐፍ``, ``ተማሪዎች -> ተማሪ``."""
|
|
173
|
+
stem = self.analyze(word).stem
|
|
174
|
+
return self._irregular.get(stem, stem)
|
|
175
|
+
|
|
176
|
+
# -- tokenization ----------------------------------------------------
|
|
177
|
+
def tokenize_spans(self, text: str, *, include_punct: bool = True) -> list[Token]:
|
|
178
|
+
"""Word-level tokens with offsets (into the normalised text)."""
|
|
179
|
+
text = self._prep(text)
|
|
180
|
+
tokens = []
|
|
181
|
+
for m in _TOKEN_RE.finditer(text):
|
|
182
|
+
kind = _KINDS[m.lastgroup] # type: ignore[index]
|
|
183
|
+
if kind == "punct" and not include_punct:
|
|
184
|
+
continue
|
|
185
|
+
tokens.append(Token(m.group(), m.start(), m.end(), kind))
|
|
186
|
+
return tokens
|
|
187
|
+
|
|
188
|
+
def tokenize(
|
|
189
|
+
self,
|
|
190
|
+
text: str,
|
|
191
|
+
*,
|
|
192
|
+
morphemes: bool = False,
|
|
193
|
+
include_punct: bool = True,
|
|
194
|
+
mark_affixes: bool = False,
|
|
195
|
+
) -> list[str]:
|
|
196
|
+
"""Split text into tokens.
|
|
197
|
+
|
|
198
|
+
With ``morphemes=True`` every Amharic word is further split into its
|
|
199
|
+
prefixes, stem and suffixes. ``mark_affixes=True`` writes prefixes as
|
|
200
|
+
``በ+`` and suffixes as ``+ን`` so the two can be told apart.
|
|
201
|
+
"""
|
|
202
|
+
out: list[str] = []
|
|
203
|
+
for tok in self.tokenize_spans(text, include_punct=include_punct):
|
|
204
|
+
if morphemes and tok.kind == "word":
|
|
205
|
+
a = self.analyze(tok.text)
|
|
206
|
+
if mark_affixes:
|
|
207
|
+
out.extend(p + "+" for p in a.prefixes)
|
|
208
|
+
out.append(a.stem)
|
|
209
|
+
out.extend("+" + s for s in a.suffixes)
|
|
210
|
+
else:
|
|
211
|
+
out.extend(a.morphemes)
|
|
212
|
+
else:
|
|
213
|
+
out.append(tok.text)
|
|
214
|
+
return out
|
|
215
|
+
|
|
216
|
+
def sentence_tokenize(self, text: str) -> list[str]:
|
|
217
|
+
"""Split text into sentences on ። ፧ ፨ ! ? and ፡፡."""
|
|
218
|
+
sentences: list[str] = []
|
|
219
|
+
start = 0
|
|
220
|
+
for m in _TERMINATOR_RE.finditer(text):
|
|
221
|
+
end = m.end()
|
|
222
|
+
if end == len(text) or text[end].isspace():
|
|
223
|
+
chunk = text[start:end].strip()
|
|
224
|
+
if chunk:
|
|
225
|
+
sentences.append(chunk)
|
|
226
|
+
start = end
|
|
227
|
+
tail = text[start:].strip()
|
|
228
|
+
if tail:
|
|
229
|
+
sentences.append(tail)
|
|
230
|
+
return sentences
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class AmharicStemmer:
|
|
234
|
+
"""Stemmer with the familiar NLTK shape: ``AmharicStemmer().stem("ተማሪዎችን")``."""
|
|
235
|
+
|
|
236
|
+
def __init__(self, tokenizer: AmharicTokenizer | None = None, *, aggressive: bool = True) -> None:
|
|
237
|
+
self.tokenizer = tokenizer or _default_tokenizer()
|
|
238
|
+
self.aggressive = aggressive
|
|
239
|
+
|
|
240
|
+
def stem(self, word: str) -> str:
|
|
241
|
+
"""Return the stem of one word."""
|
|
242
|
+
return self.tokenizer.stem(word, aggressive=self.aggressive)
|
|
243
|
+
|
|
244
|
+
def stem_words(self, words: Iterable[str]) -> list[str]:
|
|
245
|
+
"""Return the stems of many words."""
|
|
246
|
+
return [self.stem(w) for w in words]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@lru_cache(maxsize=1)
|
|
250
|
+
def _default_tokenizer() -> AmharicTokenizer:
|
|
251
|
+
return AmharicTokenizer()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def word_tokenize(text: str) -> list[str]:
|
|
255
|
+
"""Split text into words and punctuation (like ``nltk.word_tokenize``)."""
|
|
256
|
+
return _default_tokenizer().tokenize(text)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def sent_tokenize(text: str) -> list[str]:
|
|
260
|
+
"""Split text into sentences (like ``nltk.sent_tokenize``)."""
|
|
261
|
+
return _default_tokenizer().sentence_tokenize(text)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def stem(word: str, *, aggressive: bool = True) -> str:
|
|
265
|
+
"""Stem of one word, using the default tokenizer."""
|
|
266
|
+
return _default_tokenizer().stem(word, aggressive=aggressive)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def stem_words(words: Iterable[str], *, aggressive: bool = True) -> list[str]:
|
|
270
|
+
"""Stems of many words, using the default tokenizer."""
|
|
271
|
+
return [stem(w, aggressive=aggressive) for w in words]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def tokenize(
|
|
275
|
+
text: str,
|
|
276
|
+
*,
|
|
277
|
+
morphemes: bool = False,
|
|
278
|
+
include_punct: bool = True,
|
|
279
|
+
mark_affixes: bool = False,
|
|
280
|
+
) -> list[str]:
|
|
281
|
+
"""Tokenize ``text`` with the default tokenizer."""
|
|
282
|
+
return _default_tokenizer().tokenize(
|
|
283
|
+
text, morphemes=morphemes, include_punct=include_punct, mark_affixes=mark_affixes
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def tokenize_spans(text: str, *, include_punct: bool = True) -> list[Token]:
|
|
288
|
+
"""Tokens with offsets, using the default tokenizer."""
|
|
289
|
+
return _default_tokenizer().tokenize_spans(text, include_punct=include_punct)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def sentence_tokenize(text: str) -> list[str]:
|
|
293
|
+
"""Split ``text`` into sentences with the default tokenizer."""
|
|
294
|
+
return _default_tokenizer().sentence_tokenize(text)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def analyze(word: str) -> Analysis:
|
|
298
|
+
"""Morphological analysis of a single word."""
|
|
299
|
+
return _default_tokenizer().analyze(word)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def split_morphemes(word: str) -> list[str]:
|
|
303
|
+
"""Morphemes of a single word, using the default tokenizer."""
|
|
304
|
+
return _default_tokenizer().split_morphemes(word)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def lemma(word: str) -> str:
|
|
308
|
+
"""Dictionary form of the stem of ``word``, using the default tokenizer."""
|
|
309
|
+
return _default_tokenizer().lemma(word)
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: amharic-segmenter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sentence/word tokenization, morpheme splitting and stemming for Amharic (አማርኛ), NLTK-style and dependency-free.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Alki45/amharic-segmenter
|
|
6
|
+
Project-URL: Documentation, https://github.com/Alki45/amharic-segmenter#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/Alki45/amharic-segmenter/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Alki45/amharic-segmenter/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Ali Kibret Muhamed <alikibretmuhamed@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: amharic,ethiopic,ge'ez,low-resource,morphology,nlp,stemmer,stemming,tokenizer,አማርኛ
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build; extra == 'dev'
|
|
29
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
33
|
+
Requires-Dist: twine; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
<div align="center">
|
|
37
|
+
|
|
38
|
+
# amharic-segmenter
|
|
39
|
+
|
|
40
|
+
**Sentence and word tokenization, morpheme splitting and stemming for Amharic (አማርኛ)**
|
|
41
|
+
as easy as English NLP, built on real grammar rules, with zero dependencies.
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/amharic-segmenter/)
|
|
44
|
+
[](https://pypi.org/project/amharic-segmenter/)
|
|
45
|
+
[](https://github.com/Alki45/amharic-segmenter/actions/workflows/ci.yml)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
[](https://github.com/astral-sh/ruff)
|
|
48
|
+
|
|
49
|
+
[Install](#installation) · [Quickstart](#quickstart) · [Features](#features) · [API](#api-reference) · [CLI](#command-line) · [How it works](#how-it-works) · [Contributing](#contributing)
|
|
50
|
+
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
> **በአማርኛ:** አማርኛ ጽሑፍን ወደ ዓረፍተ ነገሮችና ቃላት የሚከፋፍል፣ እንዲሁም ቃላትን ወደ ሞርፊሞች
|
|
54
|
+
> (`በ`፣ `ከ`፣ `ለ`፣ `ን`፣ `ና`፣ `ዎች`…) የሚሰብር ቀላል የፓይቶን ጥቅል። ለማንኛውም የአማርኛ ቋንቋ ፕሮጀክት ነጻ ነው።
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Why this exists
|
|
59
|
+
|
|
60
|
+
General-purpose tokenizers split on whitespace or learn subwords from data. For Amharic that
|
|
61
|
+
leaves two problems:
|
|
62
|
+
|
|
63
|
+
1. **Ethiopic punctuation is not ASCII.** `።` (full stop), `፣` (comma), `፧` (question mark) and
|
|
64
|
+
the old word separator `፡` are missed or glued to words by generic tools.
|
|
65
|
+
2. **Amharic is morphologically rich.** Prepositions, plural, possessives, the object marker
|
|
66
|
+
`ን` and conjunctions are written *attached* to the word, so `ለተማሪዎቻችን` ("for our
|
|
67
|
+
students") is one whitespace token but five units of meaning.
|
|
68
|
+
|
|
69
|
+
`amharic-segmenter` handles both with transparent, documented rules taken from an Amharic
|
|
70
|
+
grammar book, so results are explainable and easy to improve.
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- **NLTK-style API**: `sent_tokenize`, `word_tokenize`, `stem`, `AmharicStemmer`. Works right
|
|
75
|
+
after `pip install`: no model downloads, no setup
|
|
76
|
+
- **Stemming** that strips prefixes and suffixes, also for words it has never seen
|
|
77
|
+
- **Sentence splitting** on `።`, `፧`, `፨`, `!`, `?` and the old-style `፡፡`
|
|
78
|
+
- **Word tokenization** that understands Ethiopic letters, punctuation and numerals
|
|
79
|
+
(`፩፻`), plus Latin text and decimals, and **never drops a character**
|
|
80
|
+
- **Morpheme segmentation** of nouns and noun phrases: prepositions `በ ከ ለ የ ስለ`, plural
|
|
81
|
+
(`ች`, `ዎች`), definite article, possessive suffixes, object marker `ን`, conjunctions `ና`/`ም`
|
|
82
|
+
- **Precision-first design:** words are only split when the stem is known, so names like
|
|
83
|
+
`ከበደ` and words like `ወንድም` stay intact. An opt-in `rules` mode trades precision for recall
|
|
84
|
+
- Handles **pronoun contractions** (`በኔ → በ + እኔ`), **fused feminine forms** (`ሐኪሟ → ሐኪም + ዋ`)
|
|
85
|
+
and **irregular plurals** (`መጻሕፍትን`, `ቃላቶች`), with `lemma()` to get the dictionary form
|
|
86
|
+
- **Token offsets** for aligning tokens with the original text
|
|
87
|
+
- **Optional normalization** of homophone letters (`ሐ→ሀ`, `ሠ→ሰ`, `ዐ→አ`, `ፀ→ጸ`)
|
|
88
|
+
- **Extensible lexicon**: bring your own words, no retraining needed
|
|
89
|
+
- **Command line tool**, fully typed (`py.typed`), pure Python, **no dependencies**,
|
|
90
|
+
Python 3.9+
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install amharic-segmenter
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Quickstart
|
|
99
|
+
|
|
100
|
+
If you have used NLTK on English text, you already know how to use it. Install, import, call:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from amharic_segmenter import word_tokenize, sent_tokenize, stem
|
|
104
|
+
|
|
105
|
+
text = "ተማሪዎቹ መጻሕፍትን አነበቡ። ከበደና አበበ ለተማሪዎቻቸው ሰጡ።"
|
|
106
|
+
|
|
107
|
+
sent_tokenize(text)
|
|
108
|
+
# ['ተማሪዎቹ መጻሕፍትን አነበቡ።', 'ከበደና አበበ ለተማሪዎቻቸው ሰጡ።']
|
|
109
|
+
|
|
110
|
+
word_tokenize(text)
|
|
111
|
+
# ['ተማሪዎቹ', 'መጻሕፍትን', 'አነበቡ', '።', 'ከበደና', 'አበበ', 'ለተማሪዎቻቸው', 'ሰጡ', '።']
|
|
112
|
+
|
|
113
|
+
[stem(w) for w in word_tokenize(text)]
|
|
114
|
+
# ['ተማሪ', 'መጻሕፍት', 'አነበቡ', '።', 'ከበደ', 'አበበ', 'ተማሪ', 'ሰጡ', '።']
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Or with a stemmer object, the way you would use `PorterStemmer`:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from amharic_segmenter import AmharicStemmer
|
|
121
|
+
|
|
122
|
+
stemmer = AmharicStemmer()
|
|
123
|
+
stemmer.stem("ተማሪዎችን") # 'ተማሪ'
|
|
124
|
+
stemmer.stem_words(["ተማሪዎች", "ላሞች"]) # ['ተማሪ', 'ላም']
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| English (NLTK) | Amharic (this package) |
|
|
128
|
+
|----------------|------------------------|
|
|
129
|
+
| `nltk.sent_tokenize(text)` | `sent_tokenize(text)` |
|
|
130
|
+
| `nltk.word_tokenize(text)` | `word_tokenize(text)` |
|
|
131
|
+
| `PorterStemmer().stem(word)` | `AmharicStemmer().stem(word)` or `stem(word)` |
|
|
132
|
+
| `nltk.download("punkt")` | **nothing to download**: rules and data ship inside the package |
|
|
133
|
+
|
|
134
|
+
### More examples
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from amharic_segmenter import tokenize, sentence_tokenize
|
|
138
|
+
|
|
139
|
+
text = "ተማሪዎቹ በከተማ ውስጥ ይኖራሉ። ከበደና አበበ ለተማሪዎቻችን መጽሐፎችን ሰጡ።"
|
|
140
|
+
|
|
141
|
+
sentence_tokenize(text)
|
|
142
|
+
# ['ተማሪዎቹ በከተማ ውስጥ ይኖራሉ።', 'ከበደና አበበ ለተማሪዎቻችን መጽሐፎችን ሰጡ።']
|
|
143
|
+
|
|
144
|
+
tokenize(text)
|
|
145
|
+
# ['ተማሪዎቹ', 'በከተማ', 'ውስጥ', 'ይኖራሉ', '።', 'ከበደና', 'አበበ', 'ለተማሪዎቻችን', 'መጽሐፎችን', 'ሰጡ', '።']
|
|
146
|
+
|
|
147
|
+
tokenize(text, morphemes=True)
|
|
148
|
+
# ['ተማሪ', 'ዎች', 'ው', 'በ', 'ከተማ', 'ውስጥ', 'ይኖራሉ', '።', 'ከበደ', 'ና', 'አበበ',
|
|
149
|
+
# 'ለ', 'ተማሪ', 'ዎች', 'ችን', 'መጽሐፍ', 'ች', 'ን', 'ሰጡ', '።']
|
|
150
|
+
|
|
151
|
+
tokenize(text, morphemes=True, mark_affixes=True)
|
|
152
|
+
# ['ተማሪ', '+ዎች', '+ው', 'በ+', 'ከተማ', 'ውስጥ', 'ይኖራሉ', '።', 'ከበደ', '+ና', 'አበበ',
|
|
153
|
+
# 'ለ+', 'ተማሪ', '+ዎች', '+ችን', 'መጽሐፍ', '+ች', '+ን', 'ሰጡ', '።']
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Analyse a single word:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from amharic_segmenter import analyze
|
|
160
|
+
|
|
161
|
+
analyze("ለተማሪዎቻችን")
|
|
162
|
+
# Analysis(word='ለተማሪዎቻችን', prefixes=('ለ',), stem='ተማሪ', suffixes=('ዎች', 'ችን'))
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Token offsets:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from amharic_segmenter import tokenize_spans
|
|
169
|
+
|
|
170
|
+
tokenize_spans("ከበደን አየ።")
|
|
171
|
+
# [Token(text='ከበደን', start=0, end=4, kind='word'),
|
|
172
|
+
# Token(text='አየ', start=5, end=7, kind='word'),
|
|
173
|
+
# Token(text='።', start=7, end=8, kind='punct')]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Command line
|
|
177
|
+
|
|
178
|
+
```console
|
|
179
|
+
$ amharic-segmenter -m "ለተማሪዎቻችን መጽሐፎችን ሰጡ።"
|
|
180
|
+
ለ ተማሪ ዎች ችን መጽሐፍ ች ን ሰጡ ።
|
|
181
|
+
|
|
182
|
+
$ amharic-segmenter --sentences --json "አበበ መጣ። ከበደ ሄደ።"
|
|
183
|
+
["አበበ መጣ።", "ከበደ ሄደ።"]
|
|
184
|
+
|
|
185
|
+
$ amharic-segmenter --stem "ተማሪዎቹ መጻሕፍትን አነበቡ።"
|
|
186
|
+
ተማሪ መጻሕፍት አነበቡ ።
|
|
187
|
+
|
|
188
|
+
$ echo "ተማሪውንም አየሁ።" | amharic-segmenter -m --mark-affixes
|
|
189
|
+
ተማሪ +ው +ን +ም አየሁ ።
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Run `amharic-segmenter --help` for all options (`--strategy`, `--lexicon`, `--normalize`,
|
|
193
|
+
`--no-punct`, `--json`). `python -m amharic_segmenter` works too.
|
|
194
|
+
|
|
195
|
+
## API reference
|
|
196
|
+
|
|
197
|
+
### Module-level functions
|
|
198
|
+
|
|
199
|
+
| Function | Returns | Description |
|
|
200
|
+
|----------|---------|-------------|
|
|
201
|
+
| `sent_tokenize(text)` | `list[str]` | Sentences (NLTK-style name for `sentence_tokenize`) |
|
|
202
|
+
| `word_tokenize(text)` | `list[str]` | Words and punctuation (NLTK-style, same as `tokenize(text)`) |
|
|
203
|
+
| `stem(word, *, aggressive=True)` | `str` | Stem of one word. `aggressive=False` leaves words outside the lexicon alone |
|
|
204
|
+
| `stem_words(words)` | `list[str]` | Stems of many words |
|
|
205
|
+
| `AmharicStemmer().stem(word)` | `str` | Stemmer object, `.stem()` and `.stem_words()` |
|
|
206
|
+
| `tokenize(text, *, morphemes=False, include_punct=True, mark_affixes=False)` | `list[str]` | Split text into tokens, optionally down to morphemes |
|
|
207
|
+
| `tokenize_spans(text, *, include_punct=True)` | `list[Token]` | Word-level tokens with `start`/`end` offsets and `kind` |
|
|
208
|
+
| `sentence_tokenize(text)` | `list[str]` | Split text into sentences |
|
|
209
|
+
| `analyze(word)` | `Analysis` | Prefixes, stem and suffixes of one word |
|
|
210
|
+
| `split_morphemes(word)` | `list[str]` | Morphemes of one word |
|
|
211
|
+
| `lemma(word)` | `str` | Dictionary form of the stem (`መጻሕፍትን → መጽሐፍ`) |
|
|
212
|
+
| `normalize_homophones(text)` | `str` | Collapse `ሐ ኀ → ሀ`, `ሠ → ሰ`, `ዐ → አ`, `ፀ → ጸ` (length-preserving) |
|
|
213
|
+
|
|
214
|
+
### `AmharicTokenizer`
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from amharic_segmenter import AmharicTokenizer
|
|
218
|
+
|
|
219
|
+
tok = AmharicTokenizer(
|
|
220
|
+
lexicon=["መምህር", "ጓደኛ"], # extra stems that may be split off
|
|
221
|
+
default_lexicon=True, # include the built-in seed lexicon
|
|
222
|
+
strategy="lexicon", # "lexicon" | "rules" | "none"
|
|
223
|
+
normalize=False, # collapse homophone letters first
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
tok.tokenize("መምህራችንን አየሁ።", morphemes=True)
|
|
227
|
+
tok.add_words(["ጋዜጣ"])
|
|
228
|
+
tok = AmharicTokenizer.from_file("my_words.txt") # one word per line, '#' comments
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| `strategy` | Behaviour |
|
|
232
|
+
|------------|-----------|
|
|
233
|
+
| `"lexicon"` *(default)* | Accept a split only if the stem is in the lexicon. High precision. |
|
|
234
|
+
| `"rules"` | Also accept unknown stems of at least `min_stem` letters. Higher recall, more false splits. |
|
|
235
|
+
| `"none"` | Never split words into morphemes. |
|
|
236
|
+
|
|
237
|
+
### `Analysis` and `Token`
|
|
238
|
+
|
|
239
|
+
- `Analysis(word, prefixes, stem, suffixes)` with `.morphemes` and `.is_split`
|
|
240
|
+
- `Token(text, start, end, kind)` where `kind` is `word`, `latin`, `number`, `punct` or `other`
|
|
241
|
+
|
|
242
|
+
> **Note:** morphemes are *underlying forms*. `ልጆች` becomes `ልጅ + ች` because the plural rule
|
|
243
|
+
> changes the stem's last letter, so joining the morphemes does not always give back the
|
|
244
|
+
> surface word. Use `tokenize_spans` when you need exact offsets.
|
|
245
|
+
|
|
246
|
+
## How it works
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
raw text
|
|
250
|
+
│
|
|
251
|
+
▼
|
|
252
|
+
┌──────────────────────────┐ Layer 1: rule-based, always on
|
|
253
|
+
│ sentences and word tokens │ Ethiopic punctuation, numerals, Latin, no loss
|
|
254
|
+
└─────────────┬────────────┘
|
|
255
|
+
│ (optional, morphemes=True)
|
|
256
|
+
▼
|
|
257
|
+
┌──────────────────────────┐ Layer 2: morphology
|
|
258
|
+
│ generate candidate splits │ prefixes ← stem → plural → possessive → ን → ና/ም
|
|
259
|
+
│ accept if stem is known │ lexicon check keeps precision high
|
|
260
|
+
└──────────────────────────┘
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The morphology layer works on the structure of the Ethiopic script itself: every letter has
|
|
264
|
+
seven vowel *orders*, and the plural and possessive rules are defined in terms of them
|
|
265
|
+
(`ሰው → ሰዎች`, `አገር → አገራችሁ`). The rules are listed with book references in
|
|
266
|
+
[`docs/GRAMMAR_RULES.md`](docs/GRAMMAR_RULES.md).
|
|
267
|
+
|
|
268
|
+
The rules are based on the grammar book *አማርኛ ሠዋሰው* (1963 E.C.).
|
|
269
|
+
|
|
270
|
+
## How it relates to other Amharic tools
|
|
271
|
+
|
|
272
|
+
| Project (PyPI) | Approach | Good for |
|
|
273
|
+
|----------------|----------|----------|
|
|
274
|
+
| **`amharic-segmenter`** (this package) | Grammar rules from a printed grammar book, NLTK-style API | Sentence and word splitting, morpheme splitting, stemming, explainable output |
|
|
275
|
+
| `amharic-tokenizer` | BPE-like merges learned from a corpus | Subword vocabularies for training language models |
|
|
276
|
+
| `amseg` | Rule-based sentence and word splitter, with normalizer and romanizer | Sentence/word splitting and transliteration |
|
|
277
|
+
|
|
278
|
+
They solve different problems and can be used together, for example `amharic-segmenter` for
|
|
279
|
+
sentences and stems and a trained BPE model for model vocabularies.
|
|
280
|
+
|
|
281
|
+
## Current scope and limitations
|
|
282
|
+
|
|
283
|
+
This is an early release (`0.1`, alpha). Please read this before using it in production:
|
|
284
|
+
|
|
285
|
+
- Only **printed pages 7-110** of the source book have been used so far (chapters 1-5 and the
|
|
286
|
+
start of chapter 6; the punctuation chapter is still missing). See [`docs/BOOK_COVERAGE.md`](docs/BOOK_COVERAGE.md) for exactly what is
|
|
287
|
+
and is not covered, e.g. the book's chapter on punctuation.
|
|
288
|
+
- Stemming is a **noun-oriented** suffix stripper, like a light English stemmer. Verbs are
|
|
289
|
+
left alone rather than guessed. Prefixes (`በ ከ ለ የ`) are only stripped when the rest is a
|
|
290
|
+
known word, so unknown words lose suffixes only. For unknown nouns it can pick the wrong stem when the
|
|
291
|
+
last letter is ambiguous (`ጋዜጦች` could come from `ጋዜጣ` or `ጋዜጥ`).
|
|
292
|
+
- Morphology covers **nouns and noun phrases**. **Verb morphology is not implemented yet.**
|
|
293
|
+
- The built-in lexicon is a small seed. Unknown stems are left unsplit in the default mode,
|
|
294
|
+
so coverage grows as the lexicon grows. Add your own words or send a pull request.
|
|
295
|
+
- Some forms are ambiguous without context (a final `ም` can be a conjunction or part of the
|
|
296
|
+
word, as in `ወንድም`). The lexicon resolves most cases, not all.
|
|
297
|
+
- Rules reflect one grammar book. Modern usage and dialects may differ; native-speaker
|
|
298
|
+
review is very welcome.
|
|
299
|
+
|
|
300
|
+
## Roadmap
|
|
301
|
+
|
|
302
|
+
- [ ] Punctuation rules from chapter 7 of the source book (pages still to be added)
|
|
303
|
+
- [ ] Verb morphology (agreement, tense, negation, relative `የ…`) from an additional source
|
|
304
|
+
- [ ] Larger open lexicon and a test corpus of annotated sentences
|
|
305
|
+
- [ ] Reduplicated plurals and internal plurals as rules
|
|
306
|
+
- [ ] Subword-vocabulary export for use with ML tokenizers
|
|
307
|
+
- [ ] Evaluation scripts against annotated Amharic data
|
|
308
|
+
|
|
309
|
+
## Contributing
|
|
310
|
+
|
|
311
|
+
Contributions are very welcome, especially from Amharic speakers and linguists. The easiest
|
|
312
|
+
first contribution is adding words to
|
|
313
|
+
[`src/amharic_segmenter/data/lexicon.txt`](src/amharic_segmenter/data/lexicon.txt).
|
|
314
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, rules for adding grammar rules, and the pull
|
|
315
|
+
request checklist.
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
git clone https://github.com/Alki45/amharic-segmenter.git
|
|
319
|
+
cd amharic-segmenter
|
|
320
|
+
pip install -e ".[dev]"
|
|
321
|
+
pytest
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Citation
|
|
325
|
+
|
|
326
|
+
If you use this package in research, please cite it:
|
|
327
|
+
|
|
328
|
+
```bibtex
|
|
329
|
+
@software{amharic_segmenter,
|
|
330
|
+
title = {amharic-segmenter: Sentence, word and morpheme tokenization for Amharic},
|
|
331
|
+
author = {Ali Kibret Muhamed},
|
|
332
|
+
year = {2026},
|
|
333
|
+
url = {https://github.com/Alki45/amharic-segmenter}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## License
|
|
338
|
+
|
|
339
|
+
[MIT](LICENSE). The grammar rules are re-implemented in original code; the source book is
|
|
340
|
+
credited above and its text is not redistributed.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
amharic_segmenter/__init__.py,sha256=UqStY6cNNXRM2t0atM8qoEupn_o8BGR-qchBzJmxWr4,850
|
|
2
|
+
amharic_segmenter/__main__.py,sha256=SCuGk-L-bb1zLj15YeA4J9PvoYe60R0p2hiL5YDrvuo,51
|
|
3
|
+
amharic_segmenter/_version.py,sha256=QTYqXqSTHFRkM9TEgpDFcHvwLbvqHDqvqfQ9EiXkcAM,23
|
|
4
|
+
amharic_segmenter/charset.py,sha256=zhYpKJ-yig3jiXFbX6ic1SYFvpVzDCqXqxvSXt2vHxg,2382
|
|
5
|
+
amharic_segmenter/cli.py,sha256=l4n67KIbSr-hpnCgbml8wWIbF0EgsMeVqTDcrmstAAE,2936
|
|
6
|
+
amharic_segmenter/morphology.py,sha256=Fv3xiYXKC4NojZANYMayjV0LhUAvC6C1paN6UIvxTrg,7960
|
|
7
|
+
amharic_segmenter/normalize.py,sha256=TQuBio21vLFFmeZ4Kr82ykGOnxa0E5NDvxeT_8_Lk7c,1003
|
|
8
|
+
amharic_segmenter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
amharic_segmenter/tokenizer.py,sha256=8SrmHWZRQQkaopCGGAnF_U3YP_zsNd-mNjbAg-hyIvQ,11755
|
|
10
|
+
amharic_segmenter/data/irregular_plurals.tsv,sha256=WEXdYZX2FF5tO41iGOr-n7ozDC1JtMdO6wYe0OxFjHw,1434
|
|
11
|
+
amharic_segmenter/data/lexicon.txt,sha256=-jALh2Ot71drJJi_4zKhu0wtJn-7I2DVRlbfpt6go04,1706
|
|
12
|
+
amharic_segmenter-0.1.0.dist-info/METADATA,sha256=FGxFHPEgqK4nExtTWkZSyqWe3nBDxjOy5qsRG5LIA_k,16280
|
|
13
|
+
amharic_segmenter-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
14
|
+
amharic_segmenter-0.1.0.dist-info/entry_points.txt,sha256=hm-5Ntu_VCNDAgp71dImtTZ6i_vq-oU4TAvx37rvhPw,65
|
|
15
|
+
amharic_segmenter-0.1.0.dist-info/licenses/LICENSE,sha256=FLUUa47oUr2WLIMBokbK5o4eqmPD_P5pJqrBm193irg,1096
|
|
16
|
+
amharic_segmenter-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ali Kibret Muhamed
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|