yotext 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yotext-0.1.0/.gitattributes +1 -0
- yotext-0.1.0/.gitignore +7 -0
- yotext-0.1.0/LICENSE +21 -0
- yotext-0.1.0/PKG-INFO +91 -0
- yotext-0.1.0/README.md +69 -0
- yotext-0.1.0/pyproject.toml +34 -0
- yotext-0.1.0/src/yotext/__init__.py +11 -0
- yotext-0.1.0/src/yotext/constants.py +44 -0
- yotext-0.1.0/src/yotext/data/.gitkeep +0 -0
- yotext-0.1.0/src/yotext/standardize.py +36 -0
- yotext-0.1.0/src/yotext/tones.py +86 -0
- yotext-0.1.0/tests/test_standardize.py +43 -0
- yotext-0.1.0/tests/test_tones.py +57 -0
- yotext-0.1.0/tools/.gitkeep +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
yotext-0.1.0/.gitignore
ADDED
yotext-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adedeji Makinde
|
|
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.
|
yotext-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: yotext
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Orthographic normalization and diacritic handling for Yorùbá text
|
|
5
|
+
Project-URL: Homepage, https://github.com/adedejimakinde/yotext
|
|
6
|
+
Project-URL: Issues, https://github.com/adedejimakinde/yotext/issues
|
|
7
|
+
Author-email: Adedeji Makinde <makindeadedeji5@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: african-languages,diacritics,low-resource,nlp,text-normalization,unicode,yoruba
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# yotext
|
|
24
|
+
|
|
25
|
+
Orthographic normalization and diacritic handling for Yorùbá text. Zero runtime dependencies. Requires Python 3.9 or newer.
|
|
26
|
+
|
|
27
|
+
## Why this exists
|
|
28
|
+
|
|
29
|
+
Yorùbá text in circulation is encoded inconsistently. The underdot that marks ẹ, ọ, and ṣ shows up as U+0323, U+0329, U+0331, or U+032D depending on the keyboard and the era it was typed on. Combining marks often arrive out of canonical order too. The same word ends up as several different byte sequences that look identical on screen and compare unequal in code. `standardize()` folds all of that into one canonical form.
|
|
30
|
+
|
|
31
|
+
## Tone marks and underdots are different things
|
|
32
|
+
|
|
33
|
+
This is the part that matters most. The underdot in ẹ, ọ, and ṣ is segmental. ẹ and e are separate phonemes, and removing the underdot turns one word into a different word. Tone marks, the acute in á and the grave in à, are suprasegmental. They mark pitch on top of a vowel, and they do not change which phoneme the vowel is.
|
|
34
|
+
|
|
35
|
+
Most text preprocessing code lumps both of these under the single label "diacritics" and strips them together. That conflates two different linguistic categories, and it produces wrong output for any task that depends on the ẹ/e or ọ/o distinction.
|
|
36
|
+
|
|
37
|
+
`strip_tones()` removes tone marks and keeps the underdot. `strip_diacritics()` removes both. I kept both functions because they answer different questions, and code that only offers one of them is answering the wrong question at least some of the time.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pip install yotext
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from yotext import standardize, strip_tones, strip_diacritics, tone_pattern
|
|
49
|
+
|
|
50
|
+
# Messy input: wrong underdot codepoint (U+0329 instead of U+0323)
|
|
51
|
+
# and a tone mark placed before the underdot instead of after.
|
|
52
|
+
messy = "e\u0300\u0329ko\u0301\u0331"
|
|
53
|
+
standardize(messy)
|
|
54
|
+
# 'ẹ̀kọ́'
|
|
55
|
+
|
|
56
|
+
strip_tones("ẹ̀kọ́")
|
|
57
|
+
# 'ẹkọ' tone dropped, underdot kept, ẹ and e stay different words
|
|
58
|
+
|
|
59
|
+
strip_diacritics("ẹ̀kọ́")
|
|
60
|
+
# 'eko' fully undiacritized, the baseline used in ablation experiments
|
|
61
|
+
|
|
62
|
+
tone_pattern("bàbá")
|
|
63
|
+
# 'LH' useful for checking tone distribution across a corpus
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Two strings can look identical and still compare unequal, if one uses a precomposed vowel and the other uses a decomposed one with a non-canonical underdot codepoint.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
s1 = "\u1eb9" # precomposed ẹ
|
|
70
|
+
s2 = "e\u0329" # decomposed, non-canonical underdot
|
|
71
|
+
s1 == s2 # False
|
|
72
|
+
standardize(s1) == standardize(s2) # True
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Running `standardize()` on a corpus before deduplicating or indexing it collapses these variants so equal words compare equal.
|
|
76
|
+
|
|
77
|
+
## What it guarantees
|
|
78
|
+
|
|
79
|
+
`standardize()` is idempotent. Running it twice gives the same result as running it once.
|
|
80
|
+
|
|
81
|
+
`strip_diacritics(s)` equals `strip_diacritics(strip_tones(s))` for any input. Removing tone first and then removing what remains lands on the same undiacritized form as removing everything at once.
|
|
82
|
+
|
|
83
|
+
Combining marks always come out in canonical order, combining class 220 before combining class 230.
|
|
84
|
+
|
|
85
|
+
## What it does not do
|
|
86
|
+
|
|
87
|
+
Version 0.1 does normalization only. It does not restore diacritics. Predicting bẹ̀rẹ̀ from bere, meaning guessing which vowels carry an underdot or a tone mark from plain ASCII input, is not implemented. That is planned for a later release.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT.
|
yotext-0.1.0/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# yotext
|
|
2
|
+
|
|
3
|
+
Orthographic normalization and diacritic handling for Yorùbá text. Zero runtime dependencies. Requires Python 3.9 or newer.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
Yorùbá text in circulation is encoded inconsistently. The underdot that marks ẹ, ọ, and ṣ shows up as U+0323, U+0329, U+0331, or U+032D depending on the keyboard and the era it was typed on. Combining marks often arrive out of canonical order too. The same word ends up as several different byte sequences that look identical on screen and compare unequal in code. `standardize()` folds all of that into one canonical form.
|
|
8
|
+
|
|
9
|
+
## Tone marks and underdots are different things
|
|
10
|
+
|
|
11
|
+
This is the part that matters most. The underdot in ẹ, ọ, and ṣ is segmental. ẹ and e are separate phonemes, and removing the underdot turns one word into a different word. Tone marks, the acute in á and the grave in à, are suprasegmental. They mark pitch on top of a vowel, and they do not change which phoneme the vowel is.
|
|
12
|
+
|
|
13
|
+
Most text preprocessing code lumps both of these under the single label "diacritics" and strips them together. That conflates two different linguistic categories, and it produces wrong output for any task that depends on the ẹ/e or ọ/o distinction.
|
|
14
|
+
|
|
15
|
+
`strip_tones()` removes tone marks and keeps the underdot. `strip_diacritics()` removes both. I kept both functions because they answer different questions, and code that only offers one of them is answering the wrong question at least some of the time.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pip install yotext
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from yotext import standardize, strip_tones, strip_diacritics, tone_pattern
|
|
27
|
+
|
|
28
|
+
# Messy input: wrong underdot codepoint (U+0329 instead of U+0323)
|
|
29
|
+
# and a tone mark placed before the underdot instead of after.
|
|
30
|
+
messy = "e\u0300\u0329ko\u0301\u0331"
|
|
31
|
+
standardize(messy)
|
|
32
|
+
# 'ẹ̀kọ́'
|
|
33
|
+
|
|
34
|
+
strip_tones("ẹ̀kọ́")
|
|
35
|
+
# 'ẹkọ' tone dropped, underdot kept, ẹ and e stay different words
|
|
36
|
+
|
|
37
|
+
strip_diacritics("ẹ̀kọ́")
|
|
38
|
+
# 'eko' fully undiacritized, the baseline used in ablation experiments
|
|
39
|
+
|
|
40
|
+
tone_pattern("bàbá")
|
|
41
|
+
# 'LH' useful for checking tone distribution across a corpus
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Two strings can look identical and still compare unequal, if one uses a precomposed vowel and the other uses a decomposed one with a non-canonical underdot codepoint.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
s1 = "\u1eb9" # precomposed ẹ
|
|
48
|
+
s2 = "e\u0329" # decomposed, non-canonical underdot
|
|
49
|
+
s1 == s2 # False
|
|
50
|
+
standardize(s1) == standardize(s2) # True
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Running `standardize()` on a corpus before deduplicating or indexing it collapses these variants so equal words compare equal.
|
|
54
|
+
|
|
55
|
+
## What it guarantees
|
|
56
|
+
|
|
57
|
+
`standardize()` is idempotent. Running it twice gives the same result as running it once.
|
|
58
|
+
|
|
59
|
+
`strip_diacritics(s)` equals `strip_diacritics(strip_tones(s))` for any input. Removing tone first and then removing what remains lands on the same undiacritized form as removing everything at once.
|
|
60
|
+
|
|
61
|
+
Combining marks always come out in canonical order, combining class 220 before combining class 230.
|
|
62
|
+
|
|
63
|
+
## What it does not do
|
|
64
|
+
|
|
65
|
+
Version 0.1 does normalization only. It does not restore diacritics. Predicting bẹ̀rẹ̀ from bere, meaning guessing which vowels carry an underdot or a tone mark from plain ASCII input, is not implemented. That is planned for a later release.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yotext"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Orthographic normalization and diacritic handling for Yorùbá text"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dependencies = []
|
|
11
|
+
authors = [{name = "Adedeji Makinde", email = "makindeadedeji5@gmail.com"}]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
keywords = ["yoruba", "nlp", "unicode", "diacritics", "text-normalization", "african-languages", "low-resource"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Topic :: Text Processing :: Linguistic",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["pytest"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/adedejimakinde/yotext"
|
|
30
|
+
Issues = "https://github.com/adedejimakinde/yotext/issues"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/yotext"]
|
|
34
|
+
artifacts = ["src/yotext/data/*"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""yotext: orthographic normalization and diacritic handling for Yorùbá text.
|
|
2
|
+
|
|
3
|
+
Provides text standardization, tone-mark stripping/extraction, and
|
|
4
|
+
diacritic handling for Yorùbá orthography.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .standardize import standardize
|
|
8
|
+
from .tones import strip_tones, strip_diacritics, tone_pattern
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__all__ = ["standardize", "strip_tones", "strip_diacritics", "tone_pattern", "__version__"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Constants used across the yotext package."""
|
|
2
|
+
|
|
3
|
+
# Combining diacritical marks (Unicode combining characters).
|
|
4
|
+
#
|
|
5
|
+
# DOT_BELOW is SEGMENTAL: it distinguishes separate phonemes (e.g. e-dot-below
|
|
6
|
+
# vs e, o-dot-below vs o) and is part of the letter identity, not the tone.
|
|
7
|
+
# ACUTE, GRAVE, and MACRON are SUPRASEGMENTAL tone marks layered on top of a
|
|
8
|
+
# vowel/syllabic nasal to indicate pitch (high, low, mid). These two
|
|
9
|
+
# categories must never be conflated: stripping "diacritics" for
|
|
10
|
+
# tone-insensitive comparison must not strip DOT_BELOW, and
|
|
11
|
+
# normalizing/stripping underdots must not touch tone marks.
|
|
12
|
+
DOT_BELOW = "\u0323"
|
|
13
|
+
ACUTE = "\u0301"
|
|
14
|
+
GRAVE = "\u0300"
|
|
15
|
+
MACRON = "\u0304"
|
|
16
|
+
|
|
17
|
+
# Suprasegmental tone marks only. Does not include DOT_BELOW, which is
|
|
18
|
+
# segmental (see note above).
|
|
19
|
+
TONE_MARKS = frozenset({ACUTE, GRAVE, MACRON})
|
|
20
|
+
|
|
21
|
+
# Non-canonical combining-below marks sometimes substituted for DOT_BELOW
|
|
22
|
+
# (e.g. by fonts, keyboards, or OCR), mapped to the canonical DOT_BELOW.
|
|
23
|
+
UNDERDOT_VARIANTS = {
|
|
24
|
+
"\u0329": DOT_BELOW,
|
|
25
|
+
"\u0331": DOT_BELOW,
|
|
26
|
+
"\u032D": DOT_BELOW,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Base Latin vowel letters (uppercase and lowercase), before any diacritics.
|
|
30
|
+
VOWELS = frozenset("aeiouAEIOU")
|
|
31
|
+
|
|
32
|
+
# Syllabic nasal consonant letters (uppercase and lowercase).
|
|
33
|
+
NASALS = frozenset("nmNM")
|
|
34
|
+
|
|
35
|
+
# Invisible/zero-width Unicode characters to strip during cleanup, mapped
|
|
36
|
+
# via str.translate (values are None to delete).
|
|
37
|
+
INVISIBLES = dict.fromkeys(map(ord, "\u200b\u200c\u200d\ufeff\u00ad"), None)
|
|
38
|
+
|
|
39
|
+
# Typographic punctuation variants normalized to their plain ASCII forms.
|
|
40
|
+
PUNCT_FIXES = {
|
|
41
|
+
"\u2018": "'", "\u2019": "'",
|
|
42
|
+
"\u201c": '"', "\u201d": '"',
|
|
43
|
+
"\u2013": "-", "\u2014": "-",
|
|
44
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Orthographic standardization utilities for Yorùbá text."""
|
|
2
|
+
|
|
3
|
+
import unicodedata
|
|
4
|
+
|
|
5
|
+
from .constants import INVISIBLES, PUNCT_FIXES, UNDERDOT_VARIANTS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def standardize(text: str, *, compose: bool = True) -> str:
|
|
9
|
+
"""Normalize Yorùbá text to a canonical orthographic form.
|
|
10
|
+
|
|
11
|
+
Invisible characters are dropped, smart quotes/dashes are replaced with
|
|
12
|
+
their plain ASCII equivalents, and non-canonical combining-below marks
|
|
13
|
+
are folded onto the canonical DOT_BELOW. The text is renormalized to NFD
|
|
14
|
+
a second time after that substitution because swapping one combining
|
|
15
|
+
mark for another can leave the combining-mark sequence out of Unicode
|
|
16
|
+
canonical order: combining class 220 (below) must sort before combining
|
|
17
|
+
class 230 (above), and a substituted mark does not automatically end up
|
|
18
|
+
in the right position relative to its neighbors. Re-running NFD
|
|
19
|
+
canonically reorders the sequence so the result composes correctly.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
text: The input text.
|
|
23
|
+
compose: If True (default), return the NFC form. If False, return
|
|
24
|
+
the NFD form.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The standardized text, in NFC or NFD form depending on `compose`.
|
|
28
|
+
"""
|
|
29
|
+
text = text.translate(INVISIBLES)
|
|
30
|
+
for old, new in PUNCT_FIXES.items():
|
|
31
|
+
text = text.replace(old, new)
|
|
32
|
+
text = unicodedata.normalize("NFD", text)
|
|
33
|
+
for old, new in UNDERDOT_VARIANTS.items():
|
|
34
|
+
text = text.replace(old, new)
|
|
35
|
+
text = unicodedata.normalize("NFD", text)
|
|
36
|
+
return unicodedata.normalize("NFC", text) if compose else text
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Tone mark and diacritic handling for Yorùbá text.
|
|
2
|
+
|
|
3
|
+
Underdots are SEGMENTAL: they distinguish separate phonemes (ẹ vs e, ọ vs o)
|
|
4
|
+
and are part of a letter's identity. Tone marks (acute, grave, and the
|
|
5
|
+
implicit mid tone) are SUPRASEGMENTAL: they indicate pitch on top of a
|
|
6
|
+
vowel/syllabic nasal, independent of which phoneme it is. Because of this,
|
|
7
|
+
stripping tone marks and stripping diacritics are genuinely different
|
|
8
|
+
operations: strip_tones removes only pitch information and must leave
|
|
9
|
+
underdots untouched, while strip_diacritics removes every combining mark,
|
|
10
|
+
underdots included, collapsing segmental distinctions along with tone.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import unicodedata
|
|
14
|
+
|
|
15
|
+
from .constants import ACUTE, GRAVE, TONE_MARKS, VOWELS
|
|
16
|
+
from .standardize import standardize
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def strip_tones(text: str) -> str:
|
|
20
|
+
"""Remove suprasegmental tone marks, preserving segmental underdots.
|
|
21
|
+
|
|
22
|
+
Only characters in TONE_MARKS are removed; DOT_BELOW is not a tone mark
|
|
23
|
+
and survives, since it marks a distinct phoneme (e.g. ẹ vs e) rather than
|
|
24
|
+
pitch.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
text: The input text.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The text with tone marks removed, in NFC form.
|
|
31
|
+
"""
|
|
32
|
+
decomposed = standardize(text, compose=False)
|
|
33
|
+
filtered = "".join(c for c in decomposed if c not in TONE_MARKS)
|
|
34
|
+
return unicodedata.normalize("NFC", filtered)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def strip_diacritics(text: str) -> str:
|
|
38
|
+
"""Remove every combining mark, including segmental underdots.
|
|
39
|
+
|
|
40
|
+
Unlike strip_tones, this removes all combining characters (any character
|
|
41
|
+
for which unicodedata.combining() is non-zero), which collapses
|
|
42
|
+
segmental distinctions such as ẹ/e and ọ/o along with tone information.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
text: The input text.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The text with all combining marks removed.
|
|
49
|
+
"""
|
|
50
|
+
decomposed = standardize(text, compose=False)
|
|
51
|
+
return "".join(c for c in decomposed if not unicodedata.combining(c))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def tone_pattern(text: str) -> str:
|
|
55
|
+
"""Derive the tone pattern of a text as a string of H/M/L markers.
|
|
56
|
+
|
|
57
|
+
Walks the decomposed text; for each base character, the combining marks
|
|
58
|
+
that immediately follow it are inspected. Non-vowel bases are skipped.
|
|
59
|
+
For vowel bases, "H" is emitted if ACUTE is present among its marks, "L"
|
|
60
|
+
if GRAVE is present, and "M" otherwise (mid tone, unmarked).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
text: The input text.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A string of "H", "M", and "L" characters, one per vowel.
|
|
67
|
+
"""
|
|
68
|
+
decomposed = standardize(text, compose=False)
|
|
69
|
+
pattern = []
|
|
70
|
+
i = 0
|
|
71
|
+
n = len(decomposed)
|
|
72
|
+
while i < n:
|
|
73
|
+
base = decomposed[i]
|
|
74
|
+
i += 1
|
|
75
|
+
marks = []
|
|
76
|
+
while i < n and unicodedata.combining(decomposed[i]) != 0:
|
|
77
|
+
marks.append(decomposed[i])
|
|
78
|
+
i += 1
|
|
79
|
+
if base in VOWELS:
|
|
80
|
+
if ACUTE in marks:
|
|
81
|
+
pattern.append("H")
|
|
82
|
+
elif GRAVE in marks:
|
|
83
|
+
pattern.append("L")
|
|
84
|
+
else:
|
|
85
|
+
pattern.append("M")
|
|
86
|
+
return "".join(pattern)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Tests for yotext.standardize."""
|
|
2
|
+
|
|
3
|
+
from yotext.standardize import standardize
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_mark_ordering_reorders_combining_classes():
|
|
7
|
+
result = standardize("e\u0301\u0329", compose=False)
|
|
8
|
+
assert result == "e\u0323\u0301"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_idempotence():
|
|
12
|
+
for s in ["", "e\u0301\u0329", "plain text", "\u2018hi\u2019", "\u200bzero width"]:
|
|
13
|
+
once = standardize(s)
|
|
14
|
+
twice = standardize(once)
|
|
15
|
+
assert once == twice
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_invisibles_removed():
|
|
19
|
+
assert standardize("a\u200bb") == "ab"
|
|
20
|
+
assert standardize("a\u200cb") == "ab"
|
|
21
|
+
assert standardize("a\u200db") == "ab"
|
|
22
|
+
assert standardize("a\ufeffb") == "ab"
|
|
23
|
+
assert standardize("a\u00adb") == "ab"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_punctuation_normalized():
|
|
27
|
+
assert standardize("\u2018hi\u2019") == "'hi'"
|
|
28
|
+
assert standardize("\u201chi\u201d") == '"hi"'
|
|
29
|
+
assert standardize("a\u2013b") == "a-b"
|
|
30
|
+
assert standardize("a\u2014b") == "a-b"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_underdot_variants_fold_to_canonical():
|
|
34
|
+
for variant in ("\u0329", "\u0331", "\u032D"):
|
|
35
|
+
result = standardize("e" + variant, compose=False)
|
|
36
|
+
assert result == "e\u0323"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_compose_flag():
|
|
40
|
+
nfd = standardize("e\u0323\u0301", compose=False)
|
|
41
|
+
nfc = standardize("e\u0323\u0301", compose=True)
|
|
42
|
+
assert nfd == "e\u0323\u0301"
|
|
43
|
+
assert nfc == "\u1eb9\u0301"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Tests for yotext.tones."""
|
|
2
|
+
|
|
3
|
+
from yotext.tones import strip_tones, strip_diacritics, tone_pattern
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_strip_tones_removes_acute():
|
|
7
|
+
result = strip_tones("e\u0301")
|
|
8
|
+
assert result == "e"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_strip_tones_removes_grave():
|
|
12
|
+
result = strip_tones("e\u0300")
|
|
13
|
+
assert result == "e"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_strip_tones_removes_macron():
|
|
17
|
+
result = strip_tones("e\u0304")
|
|
18
|
+
assert result == "e"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_strip_tones_preserves_underdot():
|
|
22
|
+
result = strip_tones("\u1eb9\u0301")
|
|
23
|
+
assert result == "\u1eb9"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_strip_diacritics_removes_everything_combining():
|
|
27
|
+
result = strip_diacritics("\u1eb9\u0301")
|
|
28
|
+
assert result == "e"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_consistency_invariant():
|
|
32
|
+
for s in [
|
|
33
|
+
"\u1eb9\u0300k\u1ecd\u0301",
|
|
34
|
+
"b\u00e0b\u00e1",
|
|
35
|
+
"plain",
|
|
36
|
+
"",
|
|
37
|
+
"\u1ecd\u0300w\u1ecd\u0300",
|
|
38
|
+
"e\u0301\u0329",
|
|
39
|
+
"\u1e63\u00e9",
|
|
40
|
+
]:
|
|
41
|
+
assert strip_diacritics(s) == strip_diacritics(strip_tones(s))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_tone_pattern_known_words():
|
|
45
|
+
assert tone_pattern("\u1eb9\u0300k\u1ecd\u0301") == "LH"
|
|
46
|
+
assert tone_pattern("b\u00e0b\u00e1") == "LH"
|
|
47
|
+
assert tone_pattern("k\u00e1\u00e0r\u1ecd\u0300") == "HLL"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_tone_pattern_unmarked_vowels_yield_mid():
|
|
51
|
+
assert tone_pattern("aeiou") == "MMMMM"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_empty_string():
|
|
55
|
+
assert strip_tones("") == ""
|
|
56
|
+
assert strip_diacritics("") == ""
|
|
57
|
+
assert tone_pattern("") == ""
|
|
File without changes
|