kannada-tokenizer 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.
- kannada_tokenizer/__init__.py +6 -0
- kannada_tokenizer/cli.py +65 -0
- kannada_tokenizer/sandhi.py +499 -0
- kannada_tokenizer/tokenizer.py +74 -0
- kannada_tokenizer/transliterate.py +599 -0
- kannada_tokenizer-0.1.0.dist-info/METADATA +105 -0
- kannada_tokenizer-0.1.0.dist-info/RECORD +10 -0
- kannada_tokenizer-0.1.0.dist-info/WHEEL +5 -0
- kannada_tokenizer-0.1.0.dist-info/entry_points.txt +2 -0
- kannada_tokenizer-0.1.0.dist-info/top_level.txt +1 -0
kannada_tokenizer/cli.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Command-line interface for the Kannada tokenizer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .tokenizer import tokenize, tokenize_words
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
"""Build and return the argument parser."""
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="kannada-tokenize",
|
|
15
|
+
description="Tokenize Kannada text with optional sandhi splitting.",
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"text",
|
|
19
|
+
nargs="?",
|
|
20
|
+
default=None,
|
|
21
|
+
help="Kannada text to tokenize. Reads from stdin if omitted.",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--no-sandhi",
|
|
25
|
+
action="store_true",
|
|
26
|
+
default=False,
|
|
27
|
+
help="Disable sandhi splitting (word-level tokenization only).",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--separator",
|
|
31
|
+
"-s",
|
|
32
|
+
default="\n",
|
|
33
|
+
help="Token separator in output (default: newline).",
|
|
34
|
+
)
|
|
35
|
+
return parser
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main(argv: list[str] | None = None) -> None:
|
|
39
|
+
"""Entry point for the ``kannada-tokenize`` CLI.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
argv: Optional argument list (defaults to ``sys.argv[1:]``).
|
|
43
|
+
"""
|
|
44
|
+
parser = _build_parser()
|
|
45
|
+
args = parser.parse_args(argv)
|
|
46
|
+
|
|
47
|
+
# Determine input source
|
|
48
|
+
if args.text is not None:
|
|
49
|
+
text: str = args.text
|
|
50
|
+
elif not sys.stdin.isatty():
|
|
51
|
+
text = sys.stdin.read()
|
|
52
|
+
else:
|
|
53
|
+
parser.print_help()
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
# Select tokenization strategy
|
|
57
|
+
tok_fn = tokenize_words if args.no_sandhi else tokenize
|
|
58
|
+
tokens: list[str] = tok_fn(text)
|
|
59
|
+
|
|
60
|
+
if tokens:
|
|
61
|
+
print(args.separator.join(tokens))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sandhi.py — Rule-based Kannada sandhi splitting for Information Retrieval.
|
|
3
|
+
|
|
4
|
+
Sandhi (ಸಂಧಿ) is the euphonic combination that occurs at morpheme and word
|
|
5
|
+
boundaries in Kannada. This module implements *reverse* sandhi rules: given
|
|
6
|
+
a combined surface form it tries to recover the original components.
|
|
7
|
+
|
|
8
|
+
Design decisions
|
|
9
|
+
~~~~~~~~~~~~~~~~
|
|
10
|
+
* **ISO 15919 encoding only.** The upstream transliteration module normalises
|
|
11
|
+
all input to ISO 15919 before this module is called.
|
|
12
|
+
* **Zero external dependencies.** Pure-Python, stdlib only.
|
|
13
|
+
* **Longest-match / fewest-parts heuristic.** When multiple candidate splits
|
|
14
|
+
are possible we prefer the one that produces the fewest, longest tokens.
|
|
15
|
+
This is a good proxy for "most likely intended reading" in an IR context
|
|
16
|
+
where recall matters more than philological precision.
|
|
17
|
+
|
|
18
|
+
Kannada sandhi differs from Sanskrit sandhi in several important ways:
|
|
19
|
+
|
|
20
|
+
1. **Lōpa Sandhi (ಲೋಪ ಸಂಧಿ)** — Vowel elision: when two vowels meet at a
|
|
21
|
+
word boundary, one (usually the first) is dropped.
|
|
22
|
+
2. **Āgama Sandhi (ಆಗಮ ಸಂಧಿ)** — Insertion: a consonant ('y' or 'v') is
|
|
23
|
+
inserted between vowels to prevent hiatus.
|
|
24
|
+
3. **Ādeśa Sandhi (ಆದೇಶ ಸಂಧಿ)** — Substitution: a sound at the junction is
|
|
25
|
+
replaced by another (Guṇa-like changes in tatsama words).
|
|
26
|
+
4. **Consonant Sandhi** — Voicing assimilation and gemination at word
|
|
27
|
+
boundaries, common in Dravidian Kannada.
|
|
28
|
+
|
|
29
|
+
The rules are represented as 4-tuples:
|
|
30
|
+
|
|
31
|
+
(combined_pattern, left_part, right_part, category)
|
|
32
|
+
|
|
33
|
+
``combined_pattern`` is the string that appears at the junction in the
|
|
34
|
+
surface form. ``left_part`` is what the *end* of the first word should be
|
|
35
|
+
restored to, and ``right_part`` is what the *start* of the second word
|
|
36
|
+
should be restored to. ``category`` is one of ``'lōpa'``, ``'āgama'``,
|
|
37
|
+
``'ādeśa'``, or ``'consonant'``.
|
|
38
|
+
|
|
39
|
+
References
|
|
40
|
+
~~~~~~~~~~
|
|
41
|
+
* Kittel, *A Grammar of the Kannada Language*
|
|
42
|
+
* Narasimhachar, *Kannada Vyākaraṇa*
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
from typing import List, Tuple
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Constants — vowel / consonant inventories (ISO 15919)
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
VOWELS: set[str] = {
|
|
54
|
+
"a", "ā", "i", "ī", "u", "ū",
|
|
55
|
+
"e", "ē", "ai", "o", "ō", "au",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
SHORT_VOWELS: set[str] = {"a", "i", "u", "e", "o"}
|
|
59
|
+
|
|
60
|
+
LONG_VOWELS: set[str] = {"ā", "ī", "ū", "ē", "ō"}
|
|
61
|
+
|
|
62
|
+
DIPHTHONGS: set[str] = {"ai", "au"}
|
|
63
|
+
|
|
64
|
+
VOICED_CONSONANTS: set[str] = {
|
|
65
|
+
"g", "gh", "j", "jh", "ḍ", "ḍh", "d", "dh", "b", "bh",
|
|
66
|
+
"ṅ", "ñ", "ṇ", "n", "m",
|
|
67
|
+
"y", "r", "l", "v", "ḷ",
|
|
68
|
+
"h",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
UNVOICED_CONSONANTS: set[str] = {
|
|
72
|
+
"k", "kh", "c", "ch", "ṭ", "ṭh", "t", "th", "p", "ph",
|
|
73
|
+
"s", "ś", "ṣ",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
STOPS_VOICED: dict[str, str] = {
|
|
77
|
+
# unvoiced stop → voiced counterpart
|
|
78
|
+
"k": "g", "kh": "gh",
|
|
79
|
+
"c": "j", "ch": "jh",
|
|
80
|
+
"ṭ": "ḍ", "ṭh": "ḍh",
|
|
81
|
+
"t": "d", "th": "dh",
|
|
82
|
+
"p": "b", "ph": "bh",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
NASALS_FOR_CLASS: dict[str, str] = {
|
|
86
|
+
# class stop → class nasal
|
|
87
|
+
"k": "ṅ", "kh": "ṅ", "g": "ṅ", "gh": "ṅ",
|
|
88
|
+
"c": "ñ", "ch": "ñ", "j": "ñ", "jh": "ñ",
|
|
89
|
+
"ṭ": "ṇ", "ṭh": "ṇ", "ḍ": "ṇ", "ḍh": "ṇ",
|
|
90
|
+
"t": "n", "th": "n", "d": "n", "dh": "n",
|
|
91
|
+
"p": "m", "ph": "m", "b": "m", "bh": "m",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# SANDHI RULES — reverse (splitting) direction
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
#
|
|
98
|
+
# Each tuple is: (combined_pattern, left_part, right_part, category)
|
|
99
|
+
#
|
|
100
|
+
# ``combined_pattern`` is what we *see* at the junction in the surface form.
|
|
101
|
+
# ``left_part`` is the restored tail of the first word.
|
|
102
|
+
# ``right_part`` is the restored head of the second word.
|
|
103
|
+
# ``category`` is one of 'lōpa', 'āgama', 'ādeśa', 'consonant'.
|
|
104
|
+
#
|
|
105
|
+
# Rules are ordered longest-pattern-first so that a simple linear scan with
|
|
106
|
+
# ``str.find`` will naturally prefer the most specific match.
|
|
107
|
+
|
|
108
|
+
# ---- 1. Lōpa Sandhi (ಲೋಪ ಸಂಧಿ) — Vowel Elision ----------------------------
|
|
109
|
+
#
|
|
110
|
+
# When two vowels meet at a word boundary, one is dropped (usually the
|
|
111
|
+
# first vowel). Reversing this means: where we see a single vowel at a
|
|
112
|
+
# potential junction, we try restoring the elided vowel.
|
|
113
|
+
#
|
|
114
|
+
# Common patterns:
|
|
115
|
+
# a + a → a (first 'a' dropped)
|
|
116
|
+
# a + ā → ā
|
|
117
|
+
# a + u → u
|
|
118
|
+
# a + i → i
|
|
119
|
+
# a + e → e
|
|
120
|
+
# a + ō → ō
|
|
121
|
+
#
|
|
122
|
+
# General principle: short vowel before a different vowel is dropped.
|
|
123
|
+
|
|
124
|
+
_LOPA_SANDHI_RULES: List[Tuple[str, str, str, str]] = []
|
|
125
|
+
|
|
126
|
+
# a + a → a (identical vowel merger — most common)
|
|
127
|
+
_LOPA_SANDHI_RULES.append(("a", "a", "a", "lōpa"))
|
|
128
|
+
|
|
129
|
+
# a + long/different vowel → the second vowel survives
|
|
130
|
+
_LOPA_PAIRS: list[tuple[str, str]] = [
|
|
131
|
+
# (surface_vowel, restored_right_start)
|
|
132
|
+
("ā", "ā"),
|
|
133
|
+
("i", "i"),
|
|
134
|
+
("ī", "ī"),
|
|
135
|
+
("u", "u"),
|
|
136
|
+
("ū", "ū"),
|
|
137
|
+
("e", "e"),
|
|
138
|
+
("ē", "ē"),
|
|
139
|
+
("o", "o"),
|
|
140
|
+
("ō", "ō"),
|
|
141
|
+
("ai", "ai"),
|
|
142
|
+
("au", "au"),
|
|
143
|
+
]
|
|
144
|
+
for _surface, _right in _LOPA_PAIRS:
|
|
145
|
+
# 'a' was elided before _right: reverse means surface _surface → 'a' + _right
|
|
146
|
+
_LOPA_SANDHI_RULES.append((_surface, "a", _right, "lōpa"))
|
|
147
|
+
|
|
148
|
+
# i/ī elision before dissimilar vowels (less common but attested)
|
|
149
|
+
for _short_v, _long_v in [("i", "ī"), ("u", "ū")]:
|
|
150
|
+
for _following in ["a", "ā", "e", "ē", "o", "ō"]:
|
|
151
|
+
_LOPA_SANDHI_RULES.append((_following, _short_v, _following, "lōpa"))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---- 2. Āgama Sandhi (ಆಗಮ ಸಂಧಿ) — Insertion --------------------------------
|
|
155
|
+
#
|
|
156
|
+
# A consonant (usually 'y' or 'v') is inserted between vowels to avoid
|
|
157
|
+
# hiatus. Similar to Sanskrit yān sandhi.
|
|
158
|
+
#
|
|
159
|
+
# Patterns:
|
|
160
|
+
# i/ī + vowel → i/ī + y + vowel → reverse: 'y' at junction → boundary
|
|
161
|
+
# u/ū + vowel → u/ū + v + vowel → reverse: 'v' at junction → boundary
|
|
162
|
+
#
|
|
163
|
+
# We store these with an empty right_part; the split logic fills in the
|
|
164
|
+
# actual vowel from the surface form.
|
|
165
|
+
|
|
166
|
+
_AGAMA_SANDHI_RULES: List[Tuple[str, str, str, str]] = []
|
|
167
|
+
|
|
168
|
+
# y-āgama: after i/ī
|
|
169
|
+
for _src in ("i", "ī"):
|
|
170
|
+
_AGAMA_SANDHI_RULES.append(("y", _src, "", "āgama"))
|
|
171
|
+
|
|
172
|
+
# v-āgama: after u/ū
|
|
173
|
+
for _src in ("u", "ū"):
|
|
174
|
+
_AGAMA_SANDHI_RULES.append(("v", _src, "", "āgama"))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---- 3. Ādeśa Sandhi (ಆದೇಶ ಸಂಧಿ) — Substitution ----------------------------
|
|
178
|
+
#
|
|
179
|
+
# A sound at the junction is replaced by another. This covers Guṇa-like
|
|
180
|
+
# changes that appear in tatsama (Sanskrit-borrowed) words in Kannada.
|
|
181
|
+
#
|
|
182
|
+
# Guṇa patterns (from Sanskrit influence):
|
|
183
|
+
# a/ā + i/ī → e/ē
|
|
184
|
+
# a/ā + u/ū → o/ō
|
|
185
|
+
# a/ā + ṛ → ar
|
|
186
|
+
|
|
187
|
+
_ADESA_SANDHI_RULES: List[Tuple[str, str, str, str]] = []
|
|
188
|
+
|
|
189
|
+
# Guṇa: a/ā + i/ī → e/ē
|
|
190
|
+
for _a in ("a", "ā"):
|
|
191
|
+
for _i in ("i", "ī"):
|
|
192
|
+
_ADESA_SANDHI_RULES.append(("e", _a, _i, "ādeśa"))
|
|
193
|
+
_ADESA_SANDHI_RULES.append(("ē", _a, _i, "ādeśa"))
|
|
194
|
+
|
|
195
|
+
# Guṇa: a/ā + u/ū → o/ō
|
|
196
|
+
for _a in ("a", "ā"):
|
|
197
|
+
for _u in ("u", "ū"):
|
|
198
|
+
_ADESA_SANDHI_RULES.append(("o", _a, _u, "ādeśa"))
|
|
199
|
+
_ADESA_SANDHI_RULES.append(("ō", _a, _u, "ādeśa"))
|
|
200
|
+
|
|
201
|
+
# Guṇa: a/ā + ṛ → ar (in tatsama words)
|
|
202
|
+
_ADESA_SANDHI_RULES.append(("ar", "a", "ṛ", "ādeśa"))
|
|
203
|
+
_ADESA_SANDHI_RULES.append(("ar", "ā", "ṛ", "ādeśa"))
|
|
204
|
+
|
|
205
|
+
# Vṛddhi-like: ā + i/ī → ai; ā + u/ū → au (in tatsama words)
|
|
206
|
+
for _i in ("i", "ī"):
|
|
207
|
+
_ADESA_SANDHI_RULES.append(("ai", "ā", _i, "ādeśa"))
|
|
208
|
+
for _u in ("u", "ū"):
|
|
209
|
+
_ADESA_SANDHI_RULES.append(("au", "ā", _u, "ādeśa"))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ---- 4. Consonant Sandhi ----------------------------------------------------
|
|
213
|
+
#
|
|
214
|
+
# 4a. Voicing assimilation — an originally unvoiced final stop becomes
|
|
215
|
+
# voiced before a voiced consonant / vowel in the next word.
|
|
216
|
+
#
|
|
217
|
+
# 4b. Gemination — consonant doubling at word boundaries, very common in
|
|
218
|
+
# Dravidian Kannada (e.g., compound words often geminate the initial
|
|
219
|
+
# consonant of the second member).
|
|
220
|
+
#
|
|
221
|
+
# 4c. Nasal assimilation — a final nasal assimilates to the class of a
|
|
222
|
+
# following stop.
|
|
223
|
+
|
|
224
|
+
_CONSONANT_SANDHI_RULES: List[Tuple[str, str, str, str]] = []
|
|
225
|
+
|
|
226
|
+
# 4a. Voicing assimilation — voiced stop at end could be unvoiced original
|
|
227
|
+
for _unv, _voi in STOPS_VOICED.items():
|
|
228
|
+
_CONSONANT_SANDHI_RULES.append((_voi, _unv, "", "consonant"))
|
|
229
|
+
|
|
230
|
+
# 4b. Gemination — doubled consonant at junction → split before second
|
|
231
|
+
# This is a very productive pattern in Kannada compounds.
|
|
232
|
+
_GEMINATION_CONSONANTS: list[str] = [
|
|
233
|
+
"k", "g", "c", "j", "ṭ", "ḍ", "t", "d", "p", "b",
|
|
234
|
+
"m", "n", "ṇ", "ṅ", "ñ",
|
|
235
|
+
"y", "r", "l", "v", "ḷ",
|
|
236
|
+
"s", "ś", "ṣ", "h",
|
|
237
|
+
]
|
|
238
|
+
for _c in _GEMINATION_CONSONANTS:
|
|
239
|
+
# doubled consonant → first word ends with single, second word starts
|
|
240
|
+
# with single (the gemination was introduced at the boundary)
|
|
241
|
+
_CONSONANT_SANDHI_RULES.append((_c + _c, _c, _c, "consonant"))
|
|
242
|
+
|
|
243
|
+
# 4c. Nasal assimilation — class nasal before a stop ← original 'm' or 'n'
|
|
244
|
+
for _stop, _nasal in NASALS_FOR_CLASS.items():
|
|
245
|
+
_CONSONANT_SANDHI_RULES.append((_nasal + _stop, "m", _stop, "consonant"))
|
|
246
|
+
_CONSONANT_SANDHI_RULES.append((_nasal + _stop, "n", _stop, "consonant"))
|
|
247
|
+
|
|
248
|
+
# 4d. Final 't' + sibilant combinations (in tatsama words)
|
|
249
|
+
_CONSONANT_SANDHI_RULES.append(("cch", "t", "ś", "consonant"))
|
|
250
|
+
_CONSONANT_SANDHI_RULES.append(("cc", "t", "c", "consonant"))
|
|
251
|
+
_CONSONANT_SANDHI_RULES.append(("jj", "t", "j", "consonant"))
|
|
252
|
+
|
|
253
|
+
# 4e. Final 't' → 'd' before voiced consonant (generic catch-all)
|
|
254
|
+
_CONSONANT_SANDHI_RULES.append(("d", "t", "", "consonant"))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
# Combined rule list — sorted longest pattern first for greedy matching
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
SANDHI_RULES: List[Tuple[str, str, str, str]] = sorted(
|
|
262
|
+
_LOPA_SANDHI_RULES
|
|
263
|
+
+ _AGAMA_SANDHI_RULES
|
|
264
|
+
+ _ADESA_SANDHI_RULES
|
|
265
|
+
+ _CONSONANT_SANDHI_RULES,
|
|
266
|
+
key=lambda r: len(r[0]),
|
|
267
|
+
reverse=True,
|
|
268
|
+
)
|
|
269
|
+
"""All reverse-sandhi rules, ordered longest ``combined_pattern`` first.
|
|
270
|
+
|
|
271
|
+
Each entry is ``(combined_pattern, left_part, right_part, category)`` where
|
|
272
|
+
*combined_pattern* is the surface string at the junction, *left_part* is
|
|
273
|
+
the restored ending of the first token, *right_part* is the restored
|
|
274
|
+
beginning of the second token (empty string when context-dependent), and
|
|
275
|
+
*category* is one of ``'lōpa'``, ``'āgama'``, ``'ādeśa'``, or
|
|
276
|
+
``'consonant'``.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# Candidate generation
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
_SINGLE_VOWEL_CHARS: set[str] = {
|
|
285
|
+
"a", "ā", "i", "ī", "u", "ū", "e", "ē", "o", "ō",
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_CONSONANT_CHARS: set[str] = {
|
|
289
|
+
"k", "g", "c", "j", "ṭ", "ḍ", "t", "d", "p", "b",
|
|
290
|
+
"ṅ", "ñ", "ṇ", "n", "m",
|
|
291
|
+
"y", "r", "l", "v", "ḷ",
|
|
292
|
+
"ś", "ṣ", "s", "h",
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# Bases that form multi-char aspirated consonants with a following 'h'
|
|
296
|
+
_ASPIRATE_BASES: set[str] = {
|
|
297
|
+
"k", "g", "c", "j", "ṭ", "ḍ", "t", "d", "p", "b",
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# Categories that are vowel-type (require consonant-before-junction check)
|
|
301
|
+
_VOWEL_TYPE_CATEGORIES: set[str] = {"lōpa", "āgama", "ādeśa"}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _generate_candidates(word: str) -> list[list[str]]:
|
|
305
|
+
"""Generate all possible sandhi-split candidates for *word*.
|
|
306
|
+
|
|
307
|
+
For every position in the word where a rule's ``combined_pattern``
|
|
308
|
+
matches, we create a candidate split ``[left, right]`` by replacing the
|
|
309
|
+
combined pattern with the rule's left and right parts.
|
|
310
|
+
|
|
311
|
+
We also recurse on the *right* fragment so that chains of sandhi
|
|
312
|
+
(common in long compound words) are handled.
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
list[list[str]]
|
|
317
|
+
Each element is a list of ISO 15919 string fragments. If no rule
|
|
318
|
+
applies anywhere in the word the returned list is empty — the
|
|
319
|
+
caller should fall back to ``[word]``.
|
|
320
|
+
"""
|
|
321
|
+
candidates: list[list[str]] = []
|
|
322
|
+
|
|
323
|
+
for combined, left_part, right_part, category in SANDHI_RULES:
|
|
324
|
+
clen = len(combined)
|
|
325
|
+
if clen == 0:
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# Scan every possible junction position. A junction cannot be at
|
|
329
|
+
# position 0 (nothing on the left) or at position len(word) (nothing
|
|
330
|
+
# on the right). We need at least one character on each side of the
|
|
331
|
+
# combined pattern to form two meaningful tokens.
|
|
332
|
+
for pos in range(1, len(word) - clen + 1):
|
|
333
|
+
# Check if the combined pattern sits at this position
|
|
334
|
+
if word[pos: pos + clen] != combined:
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
# Context validation for vowel-type sandhi (lōpa, āgama, ādeśa):
|
|
338
|
+
# the character immediately before the junction must be a
|
|
339
|
+
# consonant. Vowel-type sandhi occurs at the boundary between a
|
|
340
|
+
# consonant-final stem and a vowel-initial word; the consonant
|
|
341
|
+
# before the fused/inserted element is the last consonant of
|
|
342
|
+
# word-1. Without this check, rules would spuriously match
|
|
343
|
+
# inside words.
|
|
344
|
+
if category in _VOWEL_TYPE_CATEGORIES:
|
|
345
|
+
char_before = word[pos - 1].lower()
|
|
346
|
+
if char_before not in _CONSONANT_CHARS:
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
# Guard against splitting inside ISO 15919 multi-char
|
|
350
|
+
# consonants. If char_before is 'h' and the char before
|
|
351
|
+
# *that* is a stop, then we're inside an aspirated consonant
|
|
352
|
+
# (dh, bh, kh, etc.) and this is NOT a valid junction point.
|
|
353
|
+
if (
|
|
354
|
+
char_before == "h"
|
|
355
|
+
and pos >= 2
|
|
356
|
+
and word[pos - 2].lower() in _ASPIRATE_BASES
|
|
357
|
+
):
|
|
358
|
+
continue
|
|
359
|
+
|
|
360
|
+
# Also need material after the junction for it to be a split
|
|
361
|
+
if not word[pos + clen:]:
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
# ----- reject trivial / degenerate splits -----
|
|
365
|
+
left_token = word[:pos] + left_part
|
|
366
|
+
right_token = right_part + word[pos + clen:]
|
|
367
|
+
|
|
368
|
+
if not left_token or not right_token:
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
# Record the direct two-way split
|
|
372
|
+
candidates.append([left_token, right_token])
|
|
373
|
+
|
|
374
|
+
# Recurse on the right fragment to handle compound sandhi
|
|
375
|
+
sub_candidates = _generate_candidates(right_token)
|
|
376
|
+
for sub in sub_candidates:
|
|
377
|
+
candidates.append([left_token] + sub)
|
|
378
|
+
|
|
379
|
+
return candidates
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
# Candidate scoring
|
|
384
|
+
# ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
def _score_candidate(parts: list[str]) -> float:
|
|
387
|
+
"""Score a candidate split — higher is better.
|
|
388
|
+
|
|
389
|
+
The scoring heuristic balances two goals:
|
|
390
|
+
|
|
391
|
+
1. **Fewer parts** — every additional split risks an incorrect break, so
|
|
392
|
+
we penalise the number of parts.
|
|
393
|
+
2. **Longer tokens** — very short (1-char) fragments are suspicious; we
|
|
394
|
+
reward candidates whose shortest token is longer.
|
|
395
|
+
|
|
396
|
+
The formula is intentionally simple so that it is fast and transparent::
|
|
397
|
+
|
|
398
|
+
score = min_token_length * 10 + avg_length * 2 - num_parts * 5
|
|
399
|
+
|
|
400
|
+
A more sophisticated scorer could consult a dictionary or n-gram model,
|
|
401
|
+
but for IR purposes this works well enough.
|
|
402
|
+
|
|
403
|
+
Parameters
|
|
404
|
+
----------
|
|
405
|
+
parts : list[str]
|
|
406
|
+
The candidate list of tokens.
|
|
407
|
+
|
|
408
|
+
Returns
|
|
409
|
+
-------
|
|
410
|
+
float
|
|
411
|
+
A numeric score; higher values indicate better candidates.
|
|
412
|
+
"""
|
|
413
|
+
if not parts:
|
|
414
|
+
return -1.0
|
|
415
|
+
|
|
416
|
+
num_parts = len(parts)
|
|
417
|
+
min_len = min(len(p) for p in parts)
|
|
418
|
+
avg_len = sum(len(p) for p in parts) / num_parts
|
|
419
|
+
|
|
420
|
+
# Prefer fewer parts (penalty) and longer minimum token (reward).
|
|
421
|
+
# The average length provides a secondary signal.
|
|
422
|
+
score = (min_len * 10.0) + (avg_len * 2.0) - (num_parts * 5.0)
|
|
423
|
+
return score
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
# Public API
|
|
428
|
+
# ---------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
def split_sandhi(word: str) -> list[str]:
|
|
431
|
+
"""Split a single ISO 15919-encoded Kannada word at sandhi junctions.
|
|
432
|
+
|
|
433
|
+
Uses a rule-based approach: every reverse-sandhi rule is tried at every
|
|
434
|
+
position in the word, candidate splits are scored, and the highest-scoring
|
|
435
|
+
split is returned. If no rule produces a plausible split the word is
|
|
436
|
+
returned unchanged as ``[word]``.
|
|
437
|
+
|
|
438
|
+
Parameters
|
|
439
|
+
----------
|
|
440
|
+
word : str
|
|
441
|
+
A single token in ISO 15919 transliteration.
|
|
442
|
+
|
|
443
|
+
Returns
|
|
444
|
+
-------
|
|
445
|
+
list[str]
|
|
446
|
+
One or more ISO 15919 tokens resulting from the split. Guaranteed
|
|
447
|
+
to be non-empty.
|
|
448
|
+
|
|
449
|
+
Examples
|
|
450
|
+
--------
|
|
451
|
+
>>> split_sandhi("rāmāyana")
|
|
452
|
+
['rāma', 'āyana']
|
|
453
|
+
>>> split_sandhi("dēvi") # no split needed (too short / no rule)
|
|
454
|
+
['dēvi']
|
|
455
|
+
"""
|
|
456
|
+
if not word or len(word) <= 1:
|
|
457
|
+
return [word] if word else [""]
|
|
458
|
+
|
|
459
|
+
# Short words (≤5 chars) are very unlikely to contain a real sandhi
|
|
460
|
+
# junction that splits into two meaningful tokens.
|
|
461
|
+
if len(word) <= 5:
|
|
462
|
+
return [word]
|
|
463
|
+
|
|
464
|
+
candidates = _generate_candidates(word)
|
|
465
|
+
|
|
466
|
+
if not candidates:
|
|
467
|
+
return [word]
|
|
468
|
+
|
|
469
|
+
# Filter candidates: every fragment must have at least 3 characters
|
|
470
|
+
# to avoid degenerate splits like 'dha' + 'rma' from 'dharma'.
|
|
471
|
+
_MIN_TOKEN_LEN = 3
|
|
472
|
+
viable: list[list[str]] = []
|
|
473
|
+
for candidate in candidates:
|
|
474
|
+
if any(len(p) < _MIN_TOKEN_LEN for p in candidate):
|
|
475
|
+
continue
|
|
476
|
+
viable.append(candidate)
|
|
477
|
+
|
|
478
|
+
if not viable:
|
|
479
|
+
return [word]
|
|
480
|
+
|
|
481
|
+
# Score each viable candidate and pick the best one.
|
|
482
|
+
best: list[str] = viable[0]
|
|
483
|
+
best_score = _score_candidate(viable[0])
|
|
484
|
+
|
|
485
|
+
for candidate in viable[1:]:
|
|
486
|
+
score = _score_candidate(candidate)
|
|
487
|
+
if score > best_score:
|
|
488
|
+
best_score = score
|
|
489
|
+
best = candidate
|
|
490
|
+
|
|
491
|
+
# Only accept the split if it scores reasonably well compared to
|
|
492
|
+
# keeping the word intact. This prevents spurious splits on normal
|
|
493
|
+
# words like 'kannaḍa' where lōpa rules match but the split is
|
|
494
|
+
# nonsensical. The 0.8 factor biases toward splitting for IR recall.
|
|
495
|
+
unsplit_score = _score_candidate([word])
|
|
496
|
+
if best_score < unsplit_score * 0.8:
|
|
497
|
+
return [word]
|
|
498
|
+
|
|
499
|
+
return best
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Main tokenizer module for Kannada text processing.
|
|
2
|
+
|
|
3
|
+
Mirrors the Sanskrit tokenizer architecture, adapted for Kannada script
|
|
4
|
+
(Unicode block U+0C80–U+0CFF) and ISO 15919 transliteration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from .transliterate import normalize_to_iso15919
|
|
12
|
+
from .sandhi import split_sandhi
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Punctuation pattern: Kannada-specific punctuation, Devanagari dandas,
|
|
16
|
+
# common Latin punctuation, and whitespace.
|
|
17
|
+
_SPLIT_PATTERN = re.compile(
|
|
18
|
+
r"[\s।॥\.\,\;\:\!\?\"\'\(\)\[\]\{\}\-\—\–]+"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def tokenize(text: str) -> list[str]:
|
|
23
|
+
"""Tokenize Kannada text with transliteration normalization and sandhi splitting.
|
|
24
|
+
|
|
25
|
+
Pipeline:
|
|
26
|
+
1. Normalize input to ISO 15919.
|
|
27
|
+
2. Split on whitespace and punctuation (dandas, periods, commas, etc.).
|
|
28
|
+
3. Attempt sandhi splitting on each word.
|
|
29
|
+
4. Lowercase all tokens and filter empties.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
text: Input Kannada text in any supported script/transliteration.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A flat list of lowercase token strings.
|
|
36
|
+
"""
|
|
37
|
+
if not text or not text.strip():
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
normalized: str = normalize_to_iso15919(text)
|
|
41
|
+
raw_words: list[str] = _SPLIT_PATTERN.split(normalized)
|
|
42
|
+
|
|
43
|
+
tokens: list[str] = []
|
|
44
|
+
for word in raw_words:
|
|
45
|
+
word = word.strip()
|
|
46
|
+
if not word:
|
|
47
|
+
continue
|
|
48
|
+
# Attempt sandhi splitting; returns a list of sub-tokens
|
|
49
|
+
sub_tokens: list[str] = split_sandhi(word)
|
|
50
|
+
tokens.extend(sub_tokens)
|
|
51
|
+
|
|
52
|
+
# Lowercase and final empty filter
|
|
53
|
+
return [t.lower() for t in tokens if t.strip()]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def tokenize_words(text: str) -> list[str]:
|
|
57
|
+
"""Tokenize Kannada text at word boundaries only (no sandhi splitting).
|
|
58
|
+
|
|
59
|
+
Useful when sandhi analysis is not needed or when working with
|
|
60
|
+
pre-split text.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
text: Input Kannada text in any supported script/transliteration.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A flat list of lowercase word-level tokens.
|
|
67
|
+
"""
|
|
68
|
+
if not text or not text.strip():
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
normalized: str = normalize_to_iso15919(text)
|
|
72
|
+
raw_words: list[str] = _SPLIT_PATTERN.split(normalized)
|
|
73
|
+
|
|
74
|
+
return [w.lower() for w in raw_words if w.strip()]
|
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
"""
|
|
2
|
+
transliterate.py — Bidirectional Kannada ↔ ISO 15919 transliteration.
|
|
3
|
+
|
|
4
|
+
This module provides pure-Python, zero-dependency conversion between
|
|
5
|
+
Kannada script and ISO 15919 romanization (IAST extended for Dravidian
|
|
6
|
+
languages).
|
|
7
|
+
|
|
8
|
+
Public API
|
|
9
|
+
----------
|
|
10
|
+
- kannada_to_iso15919(text) — Convert Kannada text to ISO 15919.
|
|
11
|
+
- iso15919_to_kannada(text) — Convert ISO 15919 text to Kannada.
|
|
12
|
+
- is_kannada(text) — Detect whether a string is primarily Kannada.
|
|
13
|
+
- normalize_to_iso15919(text) — Auto-detect script and return ISO 15919.
|
|
14
|
+
|
|
15
|
+
Design notes
|
|
16
|
+
------------
|
|
17
|
+
* All mapping data lives in plain dicts/tuples at module level so tables can be
|
|
18
|
+
maintained, extended, or serialised without touching logic.
|
|
19
|
+
* Unicode NFC normalisation is applied at every public entry-point.
|
|
20
|
+
* Mixed-script text (e.g. Kannada mixed with ASCII punctuation) is handled
|
|
21
|
+
gracefully: characters outside the relevant script are passed through as-is.
|
|
22
|
+
* Kannada distinguishes short e/o (ಎ/ಒ) from long ē/ō (ಏ/ಓ), unlike
|
|
23
|
+
Devanagari. The ISO 15919 standard uses macrons (ē, ō) for the long forms.
|
|
24
|
+
* Two-part vowel signs (e.g. ೊ = \u0CC6 + \u0CBE) are handled as single
|
|
25
|
+
precomposed characters after NFC normalization.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import unicodedata
|
|
31
|
+
from typing import Dict, List, Tuple
|
|
32
|
+
|
|
33
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
34
|
+
# §1 Constants — Unicode code-point ranges
|
|
35
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
# Kannada block: U+0C80 – U+0CFF
|
|
38
|
+
_KANNADA_START = 0x0C80
|
|
39
|
+
_KANNADA_END = 0x0CFF
|
|
40
|
+
|
|
41
|
+
# Virama (halant) — suppresses the inherent 'a' vowel
|
|
42
|
+
VIRAMA = "\u0CCD" # ್
|
|
43
|
+
|
|
44
|
+
# Nukta — modifies a consonant to represent a borrowed sound
|
|
45
|
+
NUKTA = "\u0CBC" # ಼
|
|
46
|
+
|
|
47
|
+
# Chandrabindu, Anusvara, Visarga
|
|
48
|
+
CHANDRABINDU = "\u0C81" # ಁ
|
|
49
|
+
ANUSVARA = "\u0C82" # ಂ
|
|
50
|
+
VISARGA = "\u0C83" # ಃ
|
|
51
|
+
|
|
52
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
53
|
+
# §2 Mapping tables — Kannada → ISO 15919
|
|
54
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
55
|
+
#
|
|
56
|
+
# Tables are split into logical groups for clarity. Every group is a
|
|
57
|
+
# plain dict mapping a *single* Kannada character (or short sequence
|
|
58
|
+
# in the case of nukta consonants) to its ISO 15919 equivalent.
|
|
59
|
+
#
|
|
60
|
+
# For consonants the ISO 15919 value does NOT include the inherent 'a';
|
|
61
|
+
# that is inserted by the transliteration engine when no mātrā or
|
|
62
|
+
# virama follows.
|
|
63
|
+
#
|
|
64
|
+
# NOTE: Kannada has a richer vowel inventory than Devanagari — it
|
|
65
|
+
# distinguishes short e/o from long ē/ō. ISO 15919 uses macrons
|
|
66
|
+
# for the long forms.
|
|
67
|
+
|
|
68
|
+
# ----- 2a. Independent vowels (svara) -----
|
|
69
|
+
# These appear at the start of a word or after another vowel.
|
|
70
|
+
VOWELS_INDEPENDENT: Dict[str, str] = {
|
|
71
|
+
"ಅ": "a", # U+0C85
|
|
72
|
+
"ಆ": "ā", # U+0C86
|
|
73
|
+
"ಇ": "i", # U+0C87
|
|
74
|
+
"ಈ": "ī", # U+0C88
|
|
75
|
+
"ಉ": "u", # U+0C89
|
|
76
|
+
"ಊ": "ū", # U+0C8A
|
|
77
|
+
"ಋ": "ṛ", # U+0C8B
|
|
78
|
+
"ೠ": "ṝ", # U+0CE0
|
|
79
|
+
"ಌ": "ḷ", # U+0C8C
|
|
80
|
+
"ೡ": "ḹ", # U+0CE1
|
|
81
|
+
"ಎ": "e", # U+0C8E — short e (Dravidian distinction!)
|
|
82
|
+
"ಏ": "ē", # U+0C8F — long ē
|
|
83
|
+
"ಐ": "ai", # U+0C90
|
|
84
|
+
"ಒ": "o", # U+0C92 — short o (Dravidian distinction!)
|
|
85
|
+
"ಓ": "ō", # U+0C93 — long ō
|
|
86
|
+
"ಔ": "au", # U+0C94
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ----- 2b. Dependent vowel signs (mātrā) -----
|
|
90
|
+
# These attach to a preceding consonant and replace the inherent 'a'.
|
|
91
|
+
# Note: there is no mātrā for 'a' because 'a' is the inherent vowel.
|
|
92
|
+
#
|
|
93
|
+
# Some Kannada vowel signs are "two-part":
|
|
94
|
+
# ೊ (U+0CCA) = ೆ (U+0CC6) + ಾ (U+0CBE)
|
|
95
|
+
# ೋ (U+0CCB) = ೇ (U+0CC7) + ಾ (U+0CBE)
|
|
96
|
+
# ೌ (U+0CCC) = ೆ (U+0CC6) + ೕ (U+0CD5)
|
|
97
|
+
# NFC normalization collapses these into the precomposed forms listed below.
|
|
98
|
+
VOWELS_DEPENDENT: Dict[str, str] = {
|
|
99
|
+
"\u0CBE": "ā", # ಾ
|
|
100
|
+
"\u0CBF": "i", # ಿ
|
|
101
|
+
"\u0CC0": "ī", # ೀ
|
|
102
|
+
"\u0CC1": "u", # ು
|
|
103
|
+
"\u0CC2": "ū", # ೂ
|
|
104
|
+
"\u0CC3": "ṛ", # ೃ
|
|
105
|
+
"\u0CC4": "ṝ", # ೄ
|
|
106
|
+
"\u0CE2": "ḷ", # ೢ
|
|
107
|
+
"\u0CE3": "ḹ", # ೣ
|
|
108
|
+
"\u0CC6": "e", # ೆ — short e
|
|
109
|
+
"\u0CC7": "ē", # ೇ — long ē
|
|
110
|
+
"\u0CC8": "ai", # ೈ
|
|
111
|
+
"\u0CCA": "o", # ೊ — short o (precomposed two-part sign)
|
|
112
|
+
"\u0CCB": "ō", # ೋ — long ō (precomposed two-part sign)
|
|
113
|
+
"\u0CCC": "au", # ೌ
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# ----- 2c. Consonants (vyañjana) -----
|
|
117
|
+
# Arranged by the traditional varga (class) system, plus Dravidian-specific
|
|
118
|
+
# consonants at the end.
|
|
119
|
+
CONSONANTS: Dict[str, str] = {
|
|
120
|
+
# -- Velar (kaṇṭhya) --
|
|
121
|
+
"ಕ": "k", # U+0C95
|
|
122
|
+
"ಖ": "kh", # U+0C96
|
|
123
|
+
"ಗ": "g", # U+0C97
|
|
124
|
+
"ಘ": "gh", # U+0C98
|
|
125
|
+
"ಙ": "ṅ", # U+0C99
|
|
126
|
+
# -- Palatal (tālavya) --
|
|
127
|
+
"ಚ": "c", # U+0C9A
|
|
128
|
+
"ಛ": "ch", # U+0C9B
|
|
129
|
+
"ಜ": "j", # U+0C9C
|
|
130
|
+
"ಝ": "jh", # U+0C9D
|
|
131
|
+
"ಞ": "ñ", # U+0C9E
|
|
132
|
+
# -- Retroflex (mūrdhanya) --
|
|
133
|
+
"ಟ": "ṭ", # U+0C9F
|
|
134
|
+
"ಠ": "ṭh", # U+0CA0
|
|
135
|
+
"ಡ": "ḍ", # U+0CA1
|
|
136
|
+
"ಢ": "ḍh", # U+0CA2
|
|
137
|
+
"ಣ": "ṇ", # U+0CA3
|
|
138
|
+
# -- Dental (dantya) --
|
|
139
|
+
"ತ": "t", # U+0CA4
|
|
140
|
+
"ಥ": "th", # U+0CA5
|
|
141
|
+
"ದ": "d", # U+0CA6
|
|
142
|
+
"ಧ": "dh", # U+0CA7
|
|
143
|
+
"ನ": "n", # U+0CA8
|
|
144
|
+
# -- Labial (oṣṭhya) --
|
|
145
|
+
"ಪ": "p", # U+0CAA
|
|
146
|
+
"ಫ": "ph", # U+0CAB
|
|
147
|
+
"ಬ": "b", # U+0CAC
|
|
148
|
+
"ಭ": "bh", # U+0CAD
|
|
149
|
+
"ಮ": "m", # U+0CAE
|
|
150
|
+
# -- Semi-vowels (antaḥstha) --
|
|
151
|
+
"ಯ": "y", # U+0CAF
|
|
152
|
+
"ರ": "r", # U+0CB0
|
|
153
|
+
"ಲ": "l", # U+0CB2
|
|
154
|
+
"ವ": "v", # U+0CB5
|
|
155
|
+
# -- Sibilants (ūṣman) --
|
|
156
|
+
"ಶ": "ś", # U+0CB6
|
|
157
|
+
"ಷ": "ṣ", # U+0CB7
|
|
158
|
+
"ಸ": "s", # U+0CB8
|
|
159
|
+
# -- Glottal --
|
|
160
|
+
"ಹ": "h", # U+0CB9
|
|
161
|
+
# -- Dravidian-specific --
|
|
162
|
+
"ಳ": "ḻ", # U+0CB3 — retroflex lateral (unique to Dravidian!)
|
|
163
|
+
"ೞ": "ẕ", # U+0CDE — retroflex approximant (archaic Kannada)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# ----- 2d. Nukta consonants (borrowed sounds) -----
|
|
167
|
+
# Each is the base consonant + nukta (U+0CBC).
|
|
168
|
+
# Less commonly used in Kannada than in Devanagari, but supported for
|
|
169
|
+
# loanword representation.
|
|
170
|
+
NUKTA_CONSONANTS: Dict[str, str] = {
|
|
171
|
+
"ಕ಼": "q", # ಕ + ಼
|
|
172
|
+
"ಖ಼": "x", # ಖ + ಼
|
|
173
|
+
"ಗ಼": "ġ", # ಗ + ಼
|
|
174
|
+
"ಜ಼": "z", # ಜ + ಼
|
|
175
|
+
"ಫ಼": "f", # ಫ + ಼
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# ----- 2e. Modifiers & signs -----
|
|
179
|
+
MODIFIERS: Dict[str, str] = {
|
|
180
|
+
ANUSVARA: "ṃ",
|
|
181
|
+
VISARGA: "ḥ",
|
|
182
|
+
CHANDRABINDU: "m̐", # nasalisation mark
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# ----- 2f. Kannada digits -----
|
|
186
|
+
DIGITS: Dict[str, str] = {
|
|
187
|
+
"೦": "0", # U+0CE6
|
|
188
|
+
"೧": "1", # U+0CE7
|
|
189
|
+
"೨": "2", # U+0CE8
|
|
190
|
+
"೩": "3", # U+0CE9
|
|
191
|
+
"೪": "4", # U+0CEA
|
|
192
|
+
"೫": "5", # U+0CEB
|
|
193
|
+
"೬": "6", # U+0CEC
|
|
194
|
+
"೭": "7", # U+0CED
|
|
195
|
+
"೮": "8", # U+0CEE
|
|
196
|
+
"೯": "9", # U+0CEF
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# ----- 2g. Punctuation -----
|
|
200
|
+
# Danda and double danda are shared across Indic scripts (Devanagari block).
|
|
201
|
+
PUNCTUATION: Dict[str, str] = {
|
|
202
|
+
"\u0964": ".", # । danda → full stop
|
|
203
|
+
"\u0965": ".", # ॥ double danda → full stop
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
207
|
+
# §3 Reverse mapping tables — ISO 15919 → Kannada
|
|
208
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
209
|
+
#
|
|
210
|
+
# Built programmatically from the forward tables where possible, with
|
|
211
|
+
# hand-tuned additions for multi-character ISO 15919 sequences that must
|
|
212
|
+
# be matched longest-first to avoid ambiguity (e.g. "kh" before "k").
|
|
213
|
+
|
|
214
|
+
def _invert(d: Dict[str, str]) -> Dict[str, str]:
|
|
215
|
+
"""Return {v: k for k, v in d.items()}, preserving first occurrence."""
|
|
216
|
+
inv: Dict[str, str] = {}
|
|
217
|
+
for k, v in d.items():
|
|
218
|
+
if v not in inv:
|
|
219
|
+
inv[v] = k
|
|
220
|
+
return inv
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# Independent vowels: ISO 15919 → Kannada independent vowel
|
|
224
|
+
_ISO15919_TO_VOWEL_INDEPENDENT: Dict[str, str] = _invert(VOWELS_INDEPENDENT)
|
|
225
|
+
|
|
226
|
+
# Dependent vowels: ISO 15919 → Kannada mātrā
|
|
227
|
+
# 'a' maps to empty string because the inherent vowel needs no mātrā.
|
|
228
|
+
_ISO15919_TO_VOWEL_DEPENDENT: Dict[str, str] = _invert(VOWELS_DEPENDENT)
|
|
229
|
+
_ISO15919_TO_VOWEL_DEPENDENT["a"] = "" # inherent vowel — no visible mātrā
|
|
230
|
+
|
|
231
|
+
# Consonants: ISO 15919 → Kannada base consonant
|
|
232
|
+
_ISO15919_TO_CONSONANT: Dict[str, str] = _invert(CONSONANTS)
|
|
233
|
+
|
|
234
|
+
# Nukta consonants
|
|
235
|
+
_ISO15919_TO_NUKTA: Dict[str, str] = _invert(NUKTA_CONSONANTS)
|
|
236
|
+
|
|
237
|
+
# Modifiers
|
|
238
|
+
_ISO15919_TO_MODIFIER: Dict[str, str] = _invert(MODIFIERS)
|
|
239
|
+
|
|
240
|
+
# Digits
|
|
241
|
+
_ISO15919_TO_DIGIT: Dict[str, str] = _invert(DIGITS)
|
|
242
|
+
|
|
243
|
+
# All ISO 15919 vowel strings (used for lookahead when deciding inherent 'a').
|
|
244
|
+
_ALL_ISO15919_VOWELS: set = set(VOWELS_INDEPENDENT.values())
|
|
245
|
+
|
|
246
|
+
# Set of all Kannada dependent-vowel (mātrā) code-points.
|
|
247
|
+
_MATRA_CODEPOINTS: set = set(VOWELS_DEPENDENT.keys())
|
|
248
|
+
|
|
249
|
+
# Set of all Kannada independent-vowel characters.
|
|
250
|
+
_INDEPENDENT_VOWEL_CODEPOINTS: set = set(VOWELS_INDEPENDENT.keys())
|
|
251
|
+
|
|
252
|
+
# Set of all Kannada consonant characters (without nukta).
|
|
253
|
+
_CONSONANT_CODEPOINTS: set = set(CONSONANTS.keys())
|
|
254
|
+
|
|
255
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
256
|
+
# §4 Helper — sorted ISO 15919 tokens for longest-match parsing
|
|
257
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
def _build_sorted_iso15919_tokens() -> List[Tuple[str, str, str]]:
|
|
260
|
+
"""
|
|
261
|
+
Return a list of (iso15919_token, kannada, category) tuples sorted by
|
|
262
|
+
descending token length so that the parser always tries the longest
|
|
263
|
+
match first (e.g. "kh" before "k", "ai" before "a").
|
|
264
|
+
|
|
265
|
+
Categories: 'consonant', 'vowel', 'modifier', 'digit', 'nukta'.
|
|
266
|
+
"""
|
|
267
|
+
tokens: List[Tuple[str, str, str]] = []
|
|
268
|
+
|
|
269
|
+
for iso, kan in _ISO15919_TO_CONSONANT.items():
|
|
270
|
+
tokens.append((iso, kan, "consonant"))
|
|
271
|
+
for iso, kan in _ISO15919_TO_NUKTA.items():
|
|
272
|
+
tokens.append((iso, kan, "nukta"))
|
|
273
|
+
for iso, kan in _ISO15919_TO_VOWEL_INDEPENDENT.items():
|
|
274
|
+
tokens.append((iso, kan, "vowel"))
|
|
275
|
+
for iso, kan in _ISO15919_TO_MODIFIER.items():
|
|
276
|
+
tokens.append((iso, kan, "modifier"))
|
|
277
|
+
for iso, kan in _ISO15919_TO_DIGIT.items():
|
|
278
|
+
tokens.append((iso, kan, "digit"))
|
|
279
|
+
|
|
280
|
+
# Sort descending by length so longest tokens are tried first
|
|
281
|
+
tokens.sort(key=lambda t: len(t[0]), reverse=True)
|
|
282
|
+
return tokens
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
_ISO15919_TOKENS: List[Tuple[str, str, str]] = _build_sorted_iso15919_tokens()
|
|
286
|
+
|
|
287
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
288
|
+
# §5 Core: Kannada → ISO 15919
|
|
289
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
def _is_kannada_char(ch: str) -> bool:
|
|
292
|
+
"""Return True if *ch* falls within the Kannada Unicode block."""
|
|
293
|
+
cp = ord(ch)
|
|
294
|
+
return _KANNADA_START <= cp <= _KANNADA_END
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def kannada_to_iso15919(text: str) -> str:
|
|
298
|
+
"""
|
|
299
|
+
Convert a Kannada string to ISO 15919.
|
|
300
|
+
|
|
301
|
+
The algorithm walks the string character-by-character, maintaining a
|
|
302
|
+
flag ``pending_a`` that tracks whether the inherent vowel 'a' should
|
|
303
|
+
be emitted after a consonant. The inherent vowel is suppressed when
|
|
304
|
+
a virama or a dependent vowel (mātrā) follows.
|
|
305
|
+
|
|
306
|
+
Non-Kannada characters (Latin letters, ASCII punctuation, …) are
|
|
307
|
+
passed through unchanged, which makes the function safe for mixed-
|
|
308
|
+
script input.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
text : str
|
|
313
|
+
Input string, possibly containing Kannada characters.
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
-------
|
|
317
|
+
str
|
|
318
|
+
ISO 15919 transliteration of *text*.
|
|
319
|
+
"""
|
|
320
|
+
# Always work with NFC-normalised text so that two-part vowel signs
|
|
321
|
+
# (e.g. ೊ = ೆ + ಾ) are collapsed into their precomposed forms,
|
|
322
|
+
# and nukta sequences (base + U+0CBC) are in canonical form.
|
|
323
|
+
text = unicodedata.normalize("NFC", text)
|
|
324
|
+
|
|
325
|
+
out: List[str] = []
|
|
326
|
+
i = 0
|
|
327
|
+
n = len(text)
|
|
328
|
+
pending_a = False # True when we've just emitted a consonant and may need 'a'
|
|
329
|
+
|
|
330
|
+
while i < n:
|
|
331
|
+
ch = text[i]
|
|
332
|
+
|
|
333
|
+
# ── Nukta consonants (two-char sequences: base + nukta) ──
|
|
334
|
+
# Check BEFORE single consonants so "ಕ಼" is matched as a unit.
|
|
335
|
+
if i + 1 < n and text[i + 1] == NUKTA:
|
|
336
|
+
pair = text[i : i + 2]
|
|
337
|
+
if pair in NUKTA_CONSONANTS:
|
|
338
|
+
# Flush any pending inherent 'a' from a previous consonant
|
|
339
|
+
if pending_a:
|
|
340
|
+
out.append("a")
|
|
341
|
+
pending_a = False
|
|
342
|
+
out.append(NUKTA_CONSONANTS[pair])
|
|
343
|
+
pending_a = True
|
|
344
|
+
i += 2
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
# ── Virama (halant) — kills the inherent vowel ──
|
|
348
|
+
if ch == VIRAMA:
|
|
349
|
+
# Cancel the pending inherent 'a'
|
|
350
|
+
pending_a = False
|
|
351
|
+
i += 1
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
# ── Dependent vowels (mātrā) — replace inherent 'a' ──
|
|
355
|
+
if ch in VOWELS_DEPENDENT:
|
|
356
|
+
# A mātrā always cancels the inherent 'a' and provides its
|
|
357
|
+
# own vowel sound.
|
|
358
|
+
pending_a = False
|
|
359
|
+
out.append(VOWELS_DEPENDENT[ch])
|
|
360
|
+
i += 1
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
# ── Consonants ──
|
|
364
|
+
if ch in CONSONANTS:
|
|
365
|
+
if pending_a:
|
|
366
|
+
out.append("a")
|
|
367
|
+
out.append(CONSONANTS[ch])
|
|
368
|
+
pending_a = True
|
|
369
|
+
i += 1
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
# ── Independent vowels ──
|
|
373
|
+
if ch in VOWELS_INDEPENDENT:
|
|
374
|
+
if pending_a:
|
|
375
|
+
out.append("a")
|
|
376
|
+
pending_a = False
|
|
377
|
+
out.append(VOWELS_INDEPENDENT[ch])
|
|
378
|
+
i += 1
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
# ── Modifiers (anusvara, visarga, chandrabindu) ──
|
|
382
|
+
if ch in MODIFIERS:
|
|
383
|
+
# Modifiers attach to the preceding syllable; flush 'a' first.
|
|
384
|
+
if pending_a:
|
|
385
|
+
out.append("a")
|
|
386
|
+
pending_a = False
|
|
387
|
+
out.append(MODIFIERS[ch])
|
|
388
|
+
i += 1
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
# ── Digits ──
|
|
392
|
+
if ch in DIGITS:
|
|
393
|
+
if pending_a:
|
|
394
|
+
out.append("a")
|
|
395
|
+
pending_a = False
|
|
396
|
+
out.append(DIGITS[ch])
|
|
397
|
+
i += 1
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
# ── Punctuation ──
|
|
401
|
+
if ch in PUNCTUATION:
|
|
402
|
+
if pending_a:
|
|
403
|
+
out.append("a")
|
|
404
|
+
pending_a = False
|
|
405
|
+
out.append(PUNCTUATION[ch])
|
|
406
|
+
i += 1
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
# ── Nukta by itself (shouldn't happen in well-formed text) ──
|
|
410
|
+
if ch == NUKTA:
|
|
411
|
+
i += 1
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
# ── Anything else (Latin, punctuation, whitespace …) ──
|
|
415
|
+
if pending_a:
|
|
416
|
+
out.append("a")
|
|
417
|
+
pending_a = False
|
|
418
|
+
out.append(ch)
|
|
419
|
+
i += 1
|
|
420
|
+
|
|
421
|
+
# If the string ends with a consonant that still has a pending 'a'
|
|
422
|
+
if pending_a:
|
|
423
|
+
out.append("a")
|
|
424
|
+
|
|
425
|
+
return "".join(out)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
429
|
+
# §6 Core: ISO 15919 → Kannada
|
|
430
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
def _is_iso15919_vowel_at(text: str, pos: int) -> Tuple[bool, str, int]:
|
|
433
|
+
"""
|
|
434
|
+
Check whether an ISO 15919 vowel token starts at *pos* in *text*.
|
|
435
|
+
|
|
436
|
+
Returns (matched, iso15919_vowel_string, length) on success,
|
|
437
|
+
or (False, '', 0) otherwise.
|
|
438
|
+
|
|
439
|
+
Uses longest-match semantics ('ai' before 'a', 'au' before 'a', etc.).
|
|
440
|
+
"""
|
|
441
|
+
# Check two-char vowels first, then one-char.
|
|
442
|
+
for length in (2, 1):
|
|
443
|
+
candidate = text[pos : pos + length].lower()
|
|
444
|
+
if candidate in _ISO15919_TO_VOWEL_INDEPENDENT:
|
|
445
|
+
return True, candidate, length
|
|
446
|
+
return False, "", 0
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def iso15919_to_kannada(text: str) -> str:
|
|
450
|
+
"""
|
|
451
|
+
Convert an ISO 15919 string to Kannada.
|
|
452
|
+
|
|
453
|
+
The parser scans *text* from left to right using longest-match
|
|
454
|
+
tokenisation. It tracks whether the current context is "after a
|
|
455
|
+
consonant" so it can choose between independent vowels and mātrā
|
|
456
|
+
forms, and inserts virama between consecutive consonants.
|
|
457
|
+
|
|
458
|
+
Parameters
|
|
459
|
+
----------
|
|
460
|
+
text : str
|
|
461
|
+
Input string in ISO 15919 (or mixed ISO 15919 / other scripts).
|
|
462
|
+
|
|
463
|
+
Returns
|
|
464
|
+
-------
|
|
465
|
+
str
|
|
466
|
+
Kannada transliteration of *text*.
|
|
467
|
+
"""
|
|
468
|
+
text = unicodedata.normalize("NFC", text)
|
|
469
|
+
|
|
470
|
+
out: List[str] = []
|
|
471
|
+
i = 0
|
|
472
|
+
n = len(text)
|
|
473
|
+
after_consonant = False # True when the last emitted char was a consonant
|
|
474
|
+
|
|
475
|
+
while i < n:
|
|
476
|
+
matched = False
|
|
477
|
+
|
|
478
|
+
# Try every token (longest first) against the current position.
|
|
479
|
+
for iso_tok, kan, category in _ISO15919_TOKENS:
|
|
480
|
+
tok_len = len(iso_tok)
|
|
481
|
+
# Case-insensitive comparison for the ISO 15919 token
|
|
482
|
+
if text[i : i + tok_len].lower() == iso_tok.lower():
|
|
483
|
+
if category in ("consonant", "nukta"):
|
|
484
|
+
if after_consonant:
|
|
485
|
+
# Previous consonant needs virama before this one
|
|
486
|
+
out.append(VIRAMA)
|
|
487
|
+
out.append(kan)
|
|
488
|
+
after_consonant = True
|
|
489
|
+
elif category == "vowel":
|
|
490
|
+
if after_consonant:
|
|
491
|
+
# Use the dependent (mātrā) form and suppress the
|
|
492
|
+
# inherent 'a'.
|
|
493
|
+
matra = _ISO15919_TO_VOWEL_DEPENDENT.get(iso_tok.lower(), "")
|
|
494
|
+
out.append(matra) # "" for 'a' → no visible mātrā
|
|
495
|
+
after_consonant = False
|
|
496
|
+
else:
|
|
497
|
+
# Word-initial or after another vowel → independent form
|
|
498
|
+
out.append(kan)
|
|
499
|
+
elif category == "modifier":
|
|
500
|
+
# Modifiers attach after the syllable
|
|
501
|
+
if after_consonant:
|
|
502
|
+
# Implicit 'a' is assumed before the modifier
|
|
503
|
+
after_consonant = False
|
|
504
|
+
out.append(kan)
|
|
505
|
+
elif category == "digit":
|
|
506
|
+
if after_consonant:
|
|
507
|
+
after_consonant = False
|
|
508
|
+
out.append(kan)
|
|
509
|
+
|
|
510
|
+
i += tok_len
|
|
511
|
+
matched = True
|
|
512
|
+
break
|
|
513
|
+
|
|
514
|
+
if not matched:
|
|
515
|
+
# Not an ISO 15919 token — pass through (spaces, ASCII punctuation, …)
|
|
516
|
+
if after_consonant:
|
|
517
|
+
after_consonant = False
|
|
518
|
+
out.append(text[i])
|
|
519
|
+
i += 1
|
|
520
|
+
|
|
521
|
+
# If the string ends mid-consonant, we must add a virama to suppress
|
|
522
|
+
# the inherent 'a'. ISO 15919 is explicit about final vowels, so a bare
|
|
523
|
+
# consonant at end-of-string means the halant form.
|
|
524
|
+
if after_consonant:
|
|
525
|
+
out.append(VIRAMA)
|
|
526
|
+
|
|
527
|
+
return "".join(out)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
531
|
+
# §7 Detection: is_kannada
|
|
532
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
def is_kannada(text: str, threshold: float = 0.5) -> bool:
|
|
535
|
+
"""
|
|
536
|
+
Return ``True`` if *text* is primarily written in Kannada script.
|
|
537
|
+
|
|
538
|
+
The heuristic counts how many *non-whitespace* characters fall inside
|
|
539
|
+
the Kannada Unicode block and returns ``True`` when the ratio meets
|
|
540
|
+
or exceeds *threshold* (default 50 %).
|
|
541
|
+
|
|
542
|
+
Parameters
|
|
543
|
+
----------
|
|
544
|
+
text : str
|
|
545
|
+
The text to inspect.
|
|
546
|
+
threshold : float
|
|
547
|
+
Minimum fraction of Kannada characters (among scriptable
|
|
548
|
+
characters) to consider the text "Kannada". Defaults to 0.5.
|
|
549
|
+
|
|
550
|
+
Returns
|
|
551
|
+
-------
|
|
552
|
+
bool
|
|
553
|
+
"""
|
|
554
|
+
if not text:
|
|
555
|
+
return False
|
|
556
|
+
|
|
557
|
+
text = unicodedata.normalize("NFC", text)
|
|
558
|
+
|
|
559
|
+
total = 0
|
|
560
|
+
kannada_count = 0
|
|
561
|
+
for ch in text:
|
|
562
|
+
# Skip whitespace for the ratio
|
|
563
|
+
if ch.isspace():
|
|
564
|
+
continue
|
|
565
|
+
total += 1
|
|
566
|
+
if _is_kannada_char(ch):
|
|
567
|
+
kannada_count += 1
|
|
568
|
+
|
|
569
|
+
if total == 0:
|
|
570
|
+
return False
|
|
571
|
+
return (kannada_count / total) >= threshold
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
575
|
+
# §8 Convenience: normalize_to_iso15919
|
|
576
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
577
|
+
|
|
578
|
+
def normalize_to_iso15919(text: str) -> str:
|
|
579
|
+
"""
|
|
580
|
+
Auto-detect the script of *text* and return its ISO 15919 representation.
|
|
581
|
+
|
|
582
|
+
- If *text* is detected as Kannada, it is transliterated to ISO 15919.
|
|
583
|
+
- Otherwise it is returned as-is (assumed to already be ISO 15919 or some
|
|
584
|
+
other Latin-based notation).
|
|
585
|
+
|
|
586
|
+
Parameters
|
|
587
|
+
----------
|
|
588
|
+
text : str
|
|
589
|
+
Input string in Kannada or ISO 15919.
|
|
590
|
+
|
|
591
|
+
Returns
|
|
592
|
+
-------
|
|
593
|
+
str
|
|
594
|
+
ISO 15919 representation of *text*.
|
|
595
|
+
"""
|
|
596
|
+
text = unicodedata.normalize("NFC", text)
|
|
597
|
+
if is_kannada(text):
|
|
598
|
+
return kannada_to_iso15919(text)
|
|
599
|
+
return text
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kannada-tokenizer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Kannada tokenizer with sandhi splitting for Information Retrieval.
|
|
5
|
+
Author: Hemanth HM
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# kannada-tokenizer
|
|
13
|
+
|
|
14
|
+
Tokenize Kannada text with sandhi splitting for Information Retrieval.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install kannada-tokenizer
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from kannada_tokenizer import tokenize
|
|
24
|
+
|
|
25
|
+
tokenize("ಕನ್ನಡ ಧರ್ಮ")
|
|
26
|
+
# ['kannaḍa', 'dharma']
|
|
27
|
+
|
|
28
|
+
tokenize("vidyālaya")
|
|
29
|
+
# ['vidyālaya']
|
|
30
|
+
|
|
31
|
+
tokenize("mahōtsava")
|
|
32
|
+
# ['maha', 'ōtsava']
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`tokenize()` normalizes to ISO 15919, splits on whitespace and punctuation, then applies reverse sandhi rules. Accepts both Kannada script and romanized input.
|
|
36
|
+
|
|
37
|
+
## Sandhi splitting
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from kannada_tokenizer.sandhi import split_sandhi
|
|
41
|
+
|
|
42
|
+
split_sandhi("rāmāyana") # lōpa sandhi: ā → a + ā
|
|
43
|
+
# ['rāma', 'āyana']
|
|
44
|
+
|
|
45
|
+
split_sandhi("kannaḍa") # no junction found
|
|
46
|
+
# ['kannaḍa']
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Rule-based engine covering lōpa sandhi (vowel elision), āgama sandhi (y/v insertion), ādeśa sandhi (guṇa-like substitution), and consonant sandhi (voicing, gemination, nasals). Uses longest-match heuristic when splits are ambiguous.
|
|
50
|
+
|
|
51
|
+
## Transliteration
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from kannada_tokenizer.transliterate import (
|
|
55
|
+
kannada_to_iso15919,
|
|
56
|
+
iso15919_to_kannada,
|
|
57
|
+
is_kannada,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
kannada_to_iso15919("ಬೆಂಗಳೂರು")
|
|
61
|
+
# 'beṃgaḻūru'
|
|
62
|
+
|
|
63
|
+
iso15919_to_kannada("kannaḍa")
|
|
64
|
+
# 'ಕನ್ನಡ'
|
|
65
|
+
|
|
66
|
+
is_kannada("ಕನ್ನಡ")
|
|
67
|
+
# True
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Handles Kannada's short/long e/o distinction (ಎ→e vs ಏ→ē) and the retroflex lateral ಳ→ḻ unique to Dravidian.
|
|
71
|
+
|
|
72
|
+
## Word-level tokenization
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from kannada_tokenizer.tokenizer import tokenize_words
|
|
76
|
+
|
|
77
|
+
tokenize_words("vidyālaya namaḥ")
|
|
78
|
+
# ['vidyālaya', 'namaḥ']
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`tokenize_words()` splits on whitespace and punctuation only — no sandhi splitting.
|
|
82
|
+
|
|
83
|
+
## CLI
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
kannada-tokenize "ಕನ್ನಡ ಧರ್ಮ"
|
|
87
|
+
# kannaḍa
|
|
88
|
+
# dharma
|
|
89
|
+
|
|
90
|
+
echo "vidyālaya" | kannada-tokenize
|
|
91
|
+
# vidyālaya
|
|
92
|
+
|
|
93
|
+
kannada-tokenize --no-sandhi "mahōtsava"
|
|
94
|
+
# mahōtsava
|
|
95
|
+
|
|
96
|
+
kannada-tokenize -s " " "dharma yōga"
|
|
97
|
+
# dharma yōga
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- `--no-sandhi` — word-level only, skip sandhi splitting
|
|
101
|
+
- `--separator SEP` — output separator (default: newline)
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT © [Hemanth.HM](https://h3manth.com)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
kannada_tokenizer/__init__.py,sha256=b60TojvmvfbcIKpAgow0jn_qHPSS6RaDzdta5yxsHzs,169
|
|
2
|
+
kannada_tokenizer/cli.py,sha256=Td9zrG3z8GWSJRUxH6b29duEQQ22QhHjiE6bPVWwl7Y,1630
|
|
3
|
+
kannada_tokenizer/sandhi.py,sha256=8_uq9aZpKy8BqFnitEazjtnZgxf4yZOfEmMQ6Tw5Xek,18292
|
|
4
|
+
kannada_tokenizer/tokenizer.py,sha256=grjTDyVzd_nM668lB_GzN-y3Nr99gBgjCfnn3nqIUEE,2173
|
|
5
|
+
kannada_tokenizer/transliterate.py,sha256=8Himvzbnr9BXjDzT2FVgOKPvlElA7ZhXMLDCRLGhe-U,22702
|
|
6
|
+
kannada_tokenizer-0.1.0.dist-info/METADATA,sha256=qOq_HJ4eKOIy5LEyNTQc8U50ZEuJQXTxj0drdha-5pE,2449
|
|
7
|
+
kannada_tokenizer-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
8
|
+
kannada_tokenizer-0.1.0.dist-info/entry_points.txt,sha256=DyNcEFJI9f5BeV3gG0l1H4VB7_4ujcpDbvx9OwphPHY,64
|
|
9
|
+
kannada_tokenizer-0.1.0.dist-info/top_level.txt,sha256=w0MkiLbHMVQDYf32CrM0hZSLgI1fjabI2BXAh_u_Bwo,18
|
|
10
|
+
kannada_tokenizer-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kannada_tokenizer
|