jayron 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.
- jayron-0.1.0/LICENSE +71 -0
- jayron-0.1.0/PKG-INFO +170 -0
- jayron-0.1.0/README.md +145 -0
- jayron-0.1.0/jayron/__init__.py +7 -0
- jayron-0.1.0/jayron/normalizers/__init__.py +14 -0
- jayron-0.1.0/jayron/normalizers/uz/__init__.py +28 -0
- jayron-0.1.0/jayron/normalizers/uz/__main__.py +75 -0
- jayron-0.1.0/jayron/normalizers/uz/converter.py +139 -0
- jayron-0.1.0/jayron/normalizers/uz/normalizer.py +484 -0
- jayron-0.1.0/jayron/normalizers/uz/numbers.py +133 -0
- jayron-0.1.0/jayron/normalizers/uz/transliterate.py +95 -0
- jayron-0.1.0/jayron/py.typed +0 -0
- jayron-0.1.0/jayron.egg-info/PKG-INFO +170 -0
- jayron-0.1.0/jayron.egg-info/SOURCES.txt +20 -0
- jayron-0.1.0/jayron.egg-info/dependency_links.txt +1 -0
- jayron-0.1.0/jayron.egg-info/entry_points.txt +2 -0
- jayron-0.1.0/jayron.egg-info/requires.txt +5 -0
- jayron-0.1.0/jayron.egg-info/top_level.txt +1 -0
- jayron-0.1.0/pyproject.toml +44 -0
- jayron-0.1.0/setup.cfg +4 -0
- jayron-0.1.0/tests/test_uz_converter.py +95 -0
- jayron-0.1.0/tests/test_uz_normalizer.py +238 -0
jayron-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
jayron License (Non-Commercial, No Derivatives)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rakhimberdi Rakhmonberdiev. All rights reserved.
|
|
4
|
+
|
|
5
|
+
The jayron software, including its source code, word lists, rules, regular
|
|
6
|
+
expressions and documentation (the "Software"), is source-available but is
|
|
7
|
+
NOT open source. You may read the source code, but you may use it only as
|
|
8
|
+
described below.
|
|
9
|
+
|
|
10
|
+
1. FREE NON-COMMERCIAL USE
|
|
11
|
+
|
|
12
|
+
You may download, install and run the unmodified Software, and use the
|
|
13
|
+
text it produces (the "Output"), free of charge, for non-commercial
|
|
14
|
+
purposes only. Non-commercial purposes are:
|
|
15
|
+
(a) personal projects that earn no money;
|
|
16
|
+
(b) study and teaching;
|
|
17
|
+
(c) research at universities, schools or non-profit research
|
|
18
|
+
organizations, including research published in papers.
|
|
19
|
+
|
|
20
|
+
If you publish work that uses the Software, please credit
|
|
21
|
+
"jayron by Rakhimberdi Rakhmonberdiev".
|
|
22
|
+
|
|
23
|
+
2. COMMERCIAL USE REQUIRES A PAID LICENSE
|
|
24
|
+
|
|
25
|
+
Any commercial use requires a separate written license from the
|
|
26
|
+
copyright holder. Commercial use includes, for example:
|
|
27
|
+
(a) any use by or for a company or other for-profit organization;
|
|
28
|
+
(b) use in, or to build, a product or service that is sold, licensed,
|
|
29
|
+
offered for a fee or funded by advertising;
|
|
30
|
+
(c) use to prepare, normalize, train, evaluate or improve data or
|
|
31
|
+
models (for example speech recognition models) that are used
|
|
32
|
+
commercially;
|
|
33
|
+
(d) use in paid consulting, freelance or contract work.
|
|
34
|
+
|
|
35
|
+
To get a commercial license, contact Rakhimberdi Rakhmonberdiev at
|
|
36
|
+
r.raxmonberdiyev10@gmail.com.
|
|
37
|
+
|
|
38
|
+
3. WHAT YOU MAY NOT DO
|
|
39
|
+
|
|
40
|
+
Unless the copyright holder has given you written permission, you may not:
|
|
41
|
+
(a) modify, translate, adapt or create derivative works of the
|
|
42
|
+
Software;
|
|
43
|
+
(b) copy the Software or any part of its source code (including its
|
|
44
|
+
rules, word lists and regular expressions) into other software,
|
|
45
|
+
documents or datasets, or re-create it from the source code;
|
|
46
|
+
(c) sell, rent, sublicense or distribute the Software, except that it
|
|
47
|
+
may be distributed unmodified, exactly as published by the
|
|
48
|
+
copyright holder, through the Python Package Index (PyPI) and its
|
|
49
|
+
mirrors;
|
|
50
|
+
(d) remove or change this license or any copyright notice.
|
|
51
|
+
|
|
52
|
+
Copies made automatically when you install or run the Software as
|
|
53
|
+
allowed in section 1 or under a commercial license are permitted.
|
|
54
|
+
|
|
55
|
+
4. TERMINATION
|
|
56
|
+
|
|
57
|
+
If you break any of these terms, your permission to use the Software
|
|
58
|
+
ends automatically, and you must stop using it and delete all copies.
|
|
59
|
+
|
|
60
|
+
5. NO WARRANTY
|
|
61
|
+
|
|
62
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
63
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
64
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
65
|
+
NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
66
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
67
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
68
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
69
|
+
|
|
70
|
+
All rights not expressly granted in this license are reserved by the
|
|
71
|
+
copyright holder.
|
jayron-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jayron
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Uzbek text normalizer for speech recognition (ASR): numbers, dates, Cyrillic, apostrophes, new 2026 alphabet.
|
|
5
|
+
Author: Rakhimberdi Rakhmonberdiev
|
|
6
|
+
License-Expression: LicenseRef-jayron-NonCommercial
|
|
7
|
+
Keywords: uzbek,asr,speech-recognition,text-normalization,wer,transliteration,nlp
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
15
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
22
|
+
Requires-Dist: build; extra == "dev"
|
|
23
|
+
Requires-Dist: twine; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# jayron
|
|
27
|
+
|
|
28
|
+
Tools for Uzbek speech recognition. The first module is a text normalizer that
|
|
29
|
+
turns written Uzbek into the form a speaker says it, for ASR training targets
|
|
30
|
+
and for scoring (WER/CER). No dependencies, Python 3.9+.
|
|
31
|
+
|
|
32
|
+
> Free for non-commercial use. Commercial use requires a license, see [License](#license).
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install jayron
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from jayron.normalizers import uz_normilizer
|
|
40
|
+
|
|
41
|
+
uz_normilizer("2024-yil 15-mart kuni soat 14:30 da 1,5 mln so'm to'landi.")
|
|
42
|
+
# 'ikki ming yigirma toʻrtinchi yil oʻn beshinchi mart kuni soat oʻn toʻrt oʻttizda
|
|
43
|
+
# bir butun oʻndan besh million soʻm toʻlandi'
|
|
44
|
+
|
|
45
|
+
uz_normilizer(["Ўзбекистон", "5 ta kitob"]) # lists work too
|
|
46
|
+
# ['oʻzbekiston', 'beshta kitob']
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`uz_normalizer` is the same object under the standard spelling.
|
|
50
|
+
|
|
51
|
+
## What it does
|
|
52
|
+
|
|
53
|
+
| Input | Output |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `O‘zbekiston`, `O'zbekiston`, `O`zbekiston` | `oʻzbekiston` (one apostrophe character) |
|
|
56
|
+
| `Ўзбекистон Республикаси` | `oʻzbekiston respublikasi` |
|
|
57
|
+
| `Toshkеnt` (Cyrillic `е` inside) | `toshkent` |
|
|
58
|
+
| `137`, `1 000 000`, `1,000,000` | `bir yuz oʻttiz yetti`, `bir million` |
|
|
59
|
+
| `2024-yil`, `5-sinf`, `2024 yil`, `1991 y.` | `ikki ming yigirma toʻrtinchi yil`, `beshinchi sinf`, ... |
|
|
60
|
+
| `5ta`, `5 ta`, `40ga`, `10dan` | `beshta`, `beshta`, `qirqqa`, `oʻndan` |
|
|
61
|
+
| `3-xonali`, `30-yillik` | `uch xonali`, `oʻttiz yillik` (cardinal, not ordinal) |
|
|
62
|
+
| `3,5`, `2.75` | `uch butun oʻndan besh`, `ikki butun yuzdan yetmish besh` |
|
|
63
|
+
| `1/2`, `½`, `2½` | `ikkidan bir`, `ikkidan bir`, `ikki butun ikkidan bir` |
|
|
64
|
+
| `25%`, `12,5%ga` | `yigirma besh foiz`, `oʻn ikki butun oʻndan besh foizga` |
|
|
65
|
+
| `$100`, `50 €`, `100 USD`, `$1,5 mln` | `bir yuz dollar`, `ellik yevro`, ..., `bir butun oʻndan besh million dollar` |
|
|
66
|
+
| `5 kg`, `50 m²`, `-5°C` | `besh kilogramm`, `ellik kvadrat metr`, `minus besh gradus` |
|
|
67
|
+
| `14:30`, `soat 9:05da` | `oʻn toʻrt oʻttiz`, `soat toʻqqiz nol beshda` |
|
|
68
|
+
| `12.05.2024`, `2024-05-12`, `15 mart` | `oʻn ikkinchi may ikki ming yigirma toʻrtinchi yil`, ..., `oʻn beshinchi mart` |
|
|
69
|
+
| `5-10 kishi`, `1990-2000-yillar` | `besh oʻn kishi`, `... toʻqsoninchi ikki minginchi yillar` |
|
|
70
|
+
| `XXI asr`, `IV bob` | `yigirma birinchi asr`, `toʻrtinchi bob` |
|
|
71
|
+
| `+998 90 123 45 67` | read group by group |
|
|
72
|
+
| `va h.k.`, `Toshkent sh.`, `№5`, `&` | `va hokazo`, `toshkent shahri`, `raqam besh`, `va` |
|
|
73
|
+
| `[noise]`, `<unk>`, `[1]` | removed |
|
|
74
|
+
| `Café`, `«Salom!»` | `cafe`, `salom` |
|
|
75
|
+
|
|
76
|
+
With the defaults, output contains only `a-z`, `ʻ` and single spaces.
|
|
77
|
+
|
|
78
|
+
## New alphabet (2026 reform)
|
|
79
|
+
|
|
80
|
+
The reform law (signed 21 Oct 2026, in force from 22 Apr 2027) replaces
|
|
81
|
+
`Oʻ Gʻ Sh Ch` with `Ö Ğ Ş Ç` and drops `Ng` as a separate letter (it stays
|
|
82
|
+
written as n + g). The apostrophe is kept.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from jayron.normalizers import converter
|
|
86
|
+
|
|
87
|
+
converter("Bog'chaga bordingmi?") # 'Boğçaga bordingmi?'
|
|
88
|
+
converter("Ўзбекистон шаҳри") # 'Özbekiston şahri' (Cyrillic works too)
|
|
89
|
+
converter.to_old("Boğçaga bordingmi?") # 'Bogʻchaga bordingmi?'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Only those four letters change; case, punctuation, digits, `ng`, the tutuq
|
|
93
|
+
(`maʼno`) and `s'h` (`Is'hoq`) are left as they are, and so are links and
|
|
94
|
+
e-mail addresses.
|
|
95
|
+
|
|
96
|
+
To get normalized ASR text in the new alphabet, use `UzbekNormalizer(alphabet="new")`:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
UzbekNormalizer(alphabet="new")("Bog'chaga 5 ta o'quvchi bordi.")
|
|
100
|
+
# 'boğçaga beşta öquvçi bordi'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The normalizer reads either alphabet as input, so a corpus that mixes old
|
|
104
|
+
and new spellings comes out consistent.
|
|
105
|
+
|
|
106
|
+
## Options
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from jayron.normalizers import UzbekNormalizer
|
|
110
|
+
|
|
111
|
+
norm = UzbekNormalizer(
|
|
112
|
+
alphabet="old", # "new" -> ö ğ ş ç
|
|
113
|
+
apostrophe="'", # default ʻ (U+02BB); "'" if your tokenizer prefers ASCII
|
|
114
|
+
lowercase=True,
|
|
115
|
+
remove_punctuation=True, # False keeps punctuation (for punctuation-aware models)
|
|
116
|
+
transliterate=True, # Cyrillic -> Latin
|
|
117
|
+
expand_numbers=True,
|
|
118
|
+
expand_abbreviations=True, # mln, h.k., sh., %, №, &
|
|
119
|
+
strip_accents=True, # é -> e
|
|
120
|
+
drop_annotations=True, # [noise], <unk>
|
|
121
|
+
custom_rules=[(r"\bAI\b", "sun'iy intellekt")], # your own regex rules, applied first
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Use the same settings for training transcripts and for scoring (both references
|
|
126
|
+
and model output), or WER will count formatting differences as errors.
|
|
127
|
+
|
|
128
|
+
Lower-level helpers live in `jayron.normalizers.uz`: `number_to_words`,
|
|
129
|
+
`number_to_ordinal`, `cyrillic_to_latin`, `expand_numbers`.
|
|
130
|
+
|
|
131
|
+
## Command line
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
jayron-uz-norm "2024-yil 5 ta kitob"
|
|
135
|
+
jayron-uz-norm -i corpus.txt -o corpus.norm.txt
|
|
136
|
+
jayron-uz-norm -i train.jsonl -o train.norm.jsonl --jsonl --field text # NeMo manifest
|
|
137
|
+
jayron-uz-norm --help
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Conventions and known ambiguities
|
|
141
|
+
|
|
142
|
+
- Hundreds and thousands keep `bir` (`100` → `bir yuz`, `1000` → `bir ming`),
|
|
143
|
+
except when multiplying a larger scale (`100 000` → `yuz ming`) or in ordinals
|
|
144
|
+
(`100-yil` → `yuzinchi yil`).
|
|
145
|
+
- `ʻ` is used for oʻ, gʻ and the tutuq belgisi alike, so the model has one
|
|
146
|
+
apostrophe token.
|
|
147
|
+
- Space-grouped thousands are joined: `5 100` → `besh ming bir yuz`. In plain
|
|
148
|
+
text this is right almost always, but a list such as "5 100" would be merged.
|
|
149
|
+
- A single dot group is a decimal: `1.500` → `bir butun mingdan besh yuz`;
|
|
150
|
+
a single comma group of three digits is thousands: `1,500` → `bir ming besh yuz`.
|
|
151
|
+
- Times are read digit-pair by digit-pair: `14:00` → `oʻn toʻrt nol nol`.
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
pip install -e '.[dev]'
|
|
157
|
+
pytest
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
Copyright (c) 2026 Rakhimberdi Rakhmonberdiev. All rights reserved.
|
|
163
|
+
|
|
164
|
+
jayron is **free for non-commercial use**: personal projects, study, teaching
|
|
165
|
+
and non-profit research. **Commercial use** (companies, paid products or
|
|
166
|
+
services, commercial ASR models or datasets, paid contract work) **needs a
|
|
167
|
+
paid license**. Contact r.raxmonberdiyev10@gmail.com.
|
|
168
|
+
|
|
169
|
+
The source code may not be modified, copied into other projects, or
|
|
170
|
+
redistributed. See [LICENSE](LICENSE) for the full terms.
|
jayron-0.1.0/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# jayron
|
|
2
|
+
|
|
3
|
+
Tools for Uzbek speech recognition. The first module is a text normalizer that
|
|
4
|
+
turns written Uzbek into the form a speaker says it, for ASR training targets
|
|
5
|
+
and for scoring (WER/CER). No dependencies, Python 3.9+.
|
|
6
|
+
|
|
7
|
+
> Free for non-commercial use. Commercial use requires a license, see [License](#license).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install jayron
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from jayron.normalizers import uz_normilizer
|
|
15
|
+
|
|
16
|
+
uz_normilizer("2024-yil 15-mart kuni soat 14:30 da 1,5 mln so'm to'landi.")
|
|
17
|
+
# 'ikki ming yigirma toʻrtinchi yil oʻn beshinchi mart kuni soat oʻn toʻrt oʻttizda
|
|
18
|
+
# bir butun oʻndan besh million soʻm toʻlandi'
|
|
19
|
+
|
|
20
|
+
uz_normilizer(["Ўзбекистон", "5 ta kitob"]) # lists work too
|
|
21
|
+
# ['oʻzbekiston', 'beshta kitob']
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`uz_normalizer` is the same object under the standard spelling.
|
|
25
|
+
|
|
26
|
+
## What it does
|
|
27
|
+
|
|
28
|
+
| Input | Output |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `O‘zbekiston`, `O'zbekiston`, `O`zbekiston` | `oʻzbekiston` (one apostrophe character) |
|
|
31
|
+
| `Ўзбекистон Республикаси` | `oʻzbekiston respublikasi` |
|
|
32
|
+
| `Toshkеnt` (Cyrillic `е` inside) | `toshkent` |
|
|
33
|
+
| `137`, `1 000 000`, `1,000,000` | `bir yuz oʻttiz yetti`, `bir million` |
|
|
34
|
+
| `2024-yil`, `5-sinf`, `2024 yil`, `1991 y.` | `ikki ming yigirma toʻrtinchi yil`, `beshinchi sinf`, ... |
|
|
35
|
+
| `5ta`, `5 ta`, `40ga`, `10dan` | `beshta`, `beshta`, `qirqqa`, `oʻndan` |
|
|
36
|
+
| `3-xonali`, `30-yillik` | `uch xonali`, `oʻttiz yillik` (cardinal, not ordinal) |
|
|
37
|
+
| `3,5`, `2.75` | `uch butun oʻndan besh`, `ikki butun yuzdan yetmish besh` |
|
|
38
|
+
| `1/2`, `½`, `2½` | `ikkidan bir`, `ikkidan bir`, `ikki butun ikkidan bir` |
|
|
39
|
+
| `25%`, `12,5%ga` | `yigirma besh foiz`, `oʻn ikki butun oʻndan besh foizga` |
|
|
40
|
+
| `$100`, `50 €`, `100 USD`, `$1,5 mln` | `bir yuz dollar`, `ellik yevro`, ..., `bir butun oʻndan besh million dollar` |
|
|
41
|
+
| `5 kg`, `50 m²`, `-5°C` | `besh kilogramm`, `ellik kvadrat metr`, `minus besh gradus` |
|
|
42
|
+
| `14:30`, `soat 9:05da` | `oʻn toʻrt oʻttiz`, `soat toʻqqiz nol beshda` |
|
|
43
|
+
| `12.05.2024`, `2024-05-12`, `15 mart` | `oʻn ikkinchi may ikki ming yigirma toʻrtinchi yil`, ..., `oʻn beshinchi mart` |
|
|
44
|
+
| `5-10 kishi`, `1990-2000-yillar` | `besh oʻn kishi`, `... toʻqsoninchi ikki minginchi yillar` |
|
|
45
|
+
| `XXI asr`, `IV bob` | `yigirma birinchi asr`, `toʻrtinchi bob` |
|
|
46
|
+
| `+998 90 123 45 67` | read group by group |
|
|
47
|
+
| `va h.k.`, `Toshkent sh.`, `№5`, `&` | `va hokazo`, `toshkent shahri`, `raqam besh`, `va` |
|
|
48
|
+
| `[noise]`, `<unk>`, `[1]` | removed |
|
|
49
|
+
| `Café`, `«Salom!»` | `cafe`, `salom` |
|
|
50
|
+
|
|
51
|
+
With the defaults, output contains only `a-z`, `ʻ` and single spaces.
|
|
52
|
+
|
|
53
|
+
## New alphabet (2026 reform)
|
|
54
|
+
|
|
55
|
+
The reform law (signed 21 Oct 2026, in force from 22 Apr 2027) replaces
|
|
56
|
+
`Oʻ Gʻ Sh Ch` with `Ö Ğ Ş Ç` and drops `Ng` as a separate letter (it stays
|
|
57
|
+
written as n + g). The apostrophe is kept.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from jayron.normalizers import converter
|
|
61
|
+
|
|
62
|
+
converter("Bog'chaga bordingmi?") # 'Boğçaga bordingmi?'
|
|
63
|
+
converter("Ўзбекистон шаҳри") # 'Özbekiston şahri' (Cyrillic works too)
|
|
64
|
+
converter.to_old("Boğçaga bordingmi?") # 'Bogʻchaga bordingmi?'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Only those four letters change; case, punctuation, digits, `ng`, the tutuq
|
|
68
|
+
(`maʼno`) and `s'h` (`Is'hoq`) are left as they are, and so are links and
|
|
69
|
+
e-mail addresses.
|
|
70
|
+
|
|
71
|
+
To get normalized ASR text in the new alphabet, use `UzbekNormalizer(alphabet="new")`:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
UzbekNormalizer(alphabet="new")("Bog'chaga 5 ta o'quvchi bordi.")
|
|
75
|
+
# 'boğçaga beşta öquvçi bordi'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The normalizer reads either alphabet as input, so a corpus that mixes old
|
|
79
|
+
and new spellings comes out consistent.
|
|
80
|
+
|
|
81
|
+
## Options
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from jayron.normalizers import UzbekNormalizer
|
|
85
|
+
|
|
86
|
+
norm = UzbekNormalizer(
|
|
87
|
+
alphabet="old", # "new" -> ö ğ ş ç
|
|
88
|
+
apostrophe="'", # default ʻ (U+02BB); "'" if your tokenizer prefers ASCII
|
|
89
|
+
lowercase=True,
|
|
90
|
+
remove_punctuation=True, # False keeps punctuation (for punctuation-aware models)
|
|
91
|
+
transliterate=True, # Cyrillic -> Latin
|
|
92
|
+
expand_numbers=True,
|
|
93
|
+
expand_abbreviations=True, # mln, h.k., sh., %, №, &
|
|
94
|
+
strip_accents=True, # é -> e
|
|
95
|
+
drop_annotations=True, # [noise], <unk>
|
|
96
|
+
custom_rules=[(r"\bAI\b", "sun'iy intellekt")], # your own regex rules, applied first
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Use the same settings for training transcripts and for scoring (both references
|
|
101
|
+
and model output), or WER will count formatting differences as errors.
|
|
102
|
+
|
|
103
|
+
Lower-level helpers live in `jayron.normalizers.uz`: `number_to_words`,
|
|
104
|
+
`number_to_ordinal`, `cyrillic_to_latin`, `expand_numbers`.
|
|
105
|
+
|
|
106
|
+
## Command line
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
jayron-uz-norm "2024-yil 5 ta kitob"
|
|
110
|
+
jayron-uz-norm -i corpus.txt -o corpus.norm.txt
|
|
111
|
+
jayron-uz-norm -i train.jsonl -o train.norm.jsonl --jsonl --field text # NeMo manifest
|
|
112
|
+
jayron-uz-norm --help
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Conventions and known ambiguities
|
|
116
|
+
|
|
117
|
+
- Hundreds and thousands keep `bir` (`100` → `bir yuz`, `1000` → `bir ming`),
|
|
118
|
+
except when multiplying a larger scale (`100 000` → `yuz ming`) or in ordinals
|
|
119
|
+
(`100-yil` → `yuzinchi yil`).
|
|
120
|
+
- `ʻ` is used for oʻ, gʻ and the tutuq belgisi alike, so the model has one
|
|
121
|
+
apostrophe token.
|
|
122
|
+
- Space-grouped thousands are joined: `5 100` → `besh ming bir yuz`. In plain
|
|
123
|
+
text this is right almost always, but a list such as "5 100" would be merged.
|
|
124
|
+
- A single dot group is a decimal: `1.500` → `bir butun mingdan besh yuz`;
|
|
125
|
+
a single comma group of three digits is thousands: `1,500` → `bir ming besh yuz`.
|
|
126
|
+
- Times are read digit-pair by digit-pair: `14:00` → `oʻn toʻrt nol nol`.
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pip install -e '.[dev]'
|
|
132
|
+
pytest
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
Copyright (c) 2026 Rakhimberdi Rakhmonberdiev. All rights reserved.
|
|
138
|
+
|
|
139
|
+
jayron is **free for non-commercial use**: personal projects, study, teaching
|
|
140
|
+
and non-profit research. **Commercial use** (companies, paid products or
|
|
141
|
+
services, commercial ASR models or datasets, paid contract work) **needs a
|
|
142
|
+
paid license**. Contact r.raxmonberdiyev10@gmail.com.
|
|
143
|
+
|
|
144
|
+
The source code may not be modified, copied into other projects, or
|
|
145
|
+
redistributed. See [LICENSE](LICENSE) for the full terms.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Copyright (c) 2026 Rakhimberdi Rakhmonberdiev. All rights reserved.
|
|
2
|
+
# Free for non-commercial use only; modification and redistribution are not allowed.
|
|
3
|
+
# Commercial use requires a license. See the LICENSE file or contact r.raxmonberdiyev10@gmail.com.
|
|
4
|
+
|
|
5
|
+
"""jayron — tools for Uzbek speech recognition."""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright (c) 2026 Rakhimberdi Rakhmonberdiev. All rights reserved.
|
|
2
|
+
# Free for non-commercial use only; modification and redistribution are not allowed.
|
|
3
|
+
# Commercial use requires a license. See the LICENSE file or contact r.raxmonberdiyev10@gmail.com.
|
|
4
|
+
|
|
5
|
+
"""Text normalizers.
|
|
6
|
+
|
|
7
|
+
from jayron.normalizers import uz_normilizer, converter
|
|
8
|
+
uz_normilizer("2024-yil 5 ta kitob") # 'ikki ming yigirma toʻrtinchi yil beshta kitob'
|
|
9
|
+
converter("Bog'chaga bordingmi?") # 'Boğçaga bordingmi?' (new 2026 alphabet)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .uz import AlphabetConverter, UzbekNormalizer, converter, uz_normalizer, uz_normilizer
|
|
13
|
+
|
|
14
|
+
__all__ = ["AlphabetConverter", "UzbekNormalizer", "converter", "uz_normalizer", "uz_normilizer"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright (c) 2026 Rakhimberdi Rakhmonberdiev. All rights reserved.
|
|
2
|
+
# Free for non-commercial use only; modification and redistribution are not allowed.
|
|
3
|
+
# Commercial use requires a license. See the LICENSE file or contact r.raxmonberdiyev10@gmail.com.
|
|
4
|
+
|
|
5
|
+
"""Uzbek text normalization for ASR."""
|
|
6
|
+
|
|
7
|
+
from .converter import AlphabetConverter, converter, to_new, to_old
|
|
8
|
+
from .normalizer import OKINA, UzbekNormalizer, expand_numbers
|
|
9
|
+
from .numbers import number_to_ordinal, number_to_words
|
|
10
|
+
from .transliterate import cyrillic_to_latin
|
|
11
|
+
|
|
12
|
+
uz_normalizer = UzbekNormalizer()
|
|
13
|
+
uz_normilizer = uz_normalizer # same object, under the spelling used in the project
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"OKINA",
|
|
17
|
+
"AlphabetConverter",
|
|
18
|
+
"UzbekNormalizer",
|
|
19
|
+
"converter",
|
|
20
|
+
"cyrillic_to_latin",
|
|
21
|
+
"to_new",
|
|
22
|
+
"to_old",
|
|
23
|
+
"expand_numbers",
|
|
24
|
+
"number_to_ordinal",
|
|
25
|
+
"number_to_words",
|
|
26
|
+
"uz_normalizer",
|
|
27
|
+
"uz_normilizer",
|
|
28
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Copyright (c) 2026 Rakhimberdi Rakhmonberdiev. All rights reserved.
|
|
2
|
+
# Free for non-commercial use only; modification and redistribution are not allowed.
|
|
3
|
+
# Commercial use requires a license. See the LICENSE file or contact r.raxmonberdiyev10@gmail.com.
|
|
4
|
+
|
|
5
|
+
"""Command line: normalize text, text files, or JSONL manifests.
|
|
6
|
+
|
|
7
|
+
python -m jayron.normalizers.uz "2024-yil 5 ta kitob"
|
|
8
|
+
python -m jayron.normalizers.uz -i corpus.txt -o corpus.norm.txt
|
|
9
|
+
python -m jayron.normalizers.uz -i train.jsonl -o train.norm.jsonl --jsonl --field text
|
|
10
|
+
cat corpus.txt | python -m jayron.normalizers.uz
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from typing import List, Optional
|
|
19
|
+
|
|
20
|
+
from .normalizer import OKINA, UzbekNormalizer
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
24
|
+
p = argparse.ArgumentParser(prog="jayron-uz-norm", description="Uzbek text normalizer for ASR")
|
|
25
|
+
p.add_argument("text", nargs="*", help="text to normalize (otherwise reads --input or stdin)")
|
|
26
|
+
p.add_argument("-i", "--input", help="input file (one sentence per line, or JSONL with --jsonl)")
|
|
27
|
+
p.add_argument("-o", "--output", help="output file (default: stdout)")
|
|
28
|
+
p.add_argument("--jsonl", action="store_true", help="input is JSON lines (e.g. a NeMo manifest)")
|
|
29
|
+
p.add_argument("--field", default="text", help="JSONL field to normalize (default: text)")
|
|
30
|
+
p.add_argument("--out-field", help="write the result to this field instead of overwriting --field")
|
|
31
|
+
p.add_argument("--alphabet", choices=("old", "new"), default="old",
|
|
32
|
+
help="old: oʻ gʻ sh ch (default); new: ö ğ ş ç (2026 reform)")
|
|
33
|
+
p.add_argument("--apostrophe", default=OKINA, help="character for oʻ/gʻ/tutuq (default: ʻ U+02BB)")
|
|
34
|
+
p.add_argument("--keep-case", action="store_true")
|
|
35
|
+
p.add_argument("--keep-punctuation", action="store_true")
|
|
36
|
+
p.add_argument("--no-transliterate", action="store_true", help="leave Cyrillic text as is")
|
|
37
|
+
p.add_argument("--no-numbers", action="store_true", help="leave digits as is")
|
|
38
|
+
args = p.parse_args(argv)
|
|
39
|
+
|
|
40
|
+
norm = UzbekNormalizer(
|
|
41
|
+
alphabet=args.alphabet,
|
|
42
|
+
apostrophe=args.apostrophe,
|
|
43
|
+
lowercase=not args.keep_case,
|
|
44
|
+
remove_punctuation=not args.keep_punctuation,
|
|
45
|
+
transliterate=not args.no_transliterate,
|
|
46
|
+
expand_numbers=not args.no_numbers,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if args.text:
|
|
50
|
+
print(norm(" ".join(args.text)))
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
src = open(args.input, encoding="utf-8") if args.input else sys.stdin
|
|
54
|
+
dst = open(args.output, "w", encoding="utf-8") if args.output else sys.stdout
|
|
55
|
+
try:
|
|
56
|
+
for line in src:
|
|
57
|
+
line = line.rstrip("\n")
|
|
58
|
+
if args.jsonl:
|
|
59
|
+
if not line.strip():
|
|
60
|
+
continue
|
|
61
|
+
record = json.loads(line)
|
|
62
|
+
record[args.out_field or args.field] = norm(record.get(args.field) or "")
|
|
63
|
+
dst.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
64
|
+
else:
|
|
65
|
+
dst.write(norm(line) + "\n")
|
|
66
|
+
finally:
|
|
67
|
+
if src is not sys.stdin:
|
|
68
|
+
src.close()
|
|
69
|
+
if dst is not sys.stdout:
|
|
70
|
+
dst.close()
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
sys.exit(main())
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Copyright (c) 2026 Rakhimberdi Rakhmonberdiev. All rights reserved.
|
|
2
|
+
# Free for non-commercial use only; modification and redistribution are not allowed.
|
|
3
|
+
# Commercial use requires a license. See the LICENSE file or contact r.raxmonberdiyev10@gmail.com.
|
|
4
|
+
|
|
5
|
+
"""Convert Uzbek text between the 1995 Latin alphabet and the 2026 reform.
|
|
6
|
+
|
|
7
|
+
The reform (law signed 21 Oct 2026, in force from 22 Apr 2027) replaces four
|
|
8
|
+
letters and drops Ng as a separate letter:
|
|
9
|
+
|
|
10
|
+
Oʻ -> Ö Gʻ -> Ğ Sh -> Ş Ch -> Ç (ng stays written as n + g)
|
|
11
|
+
|
|
12
|
+
The apostrophe (tutuq belgisi, as in maʼno) is kept, and so is the s'h spelling
|
|
13
|
+
of a separate s + h (Is'hoq stays Is'hoq).
|
|
14
|
+
|
|
15
|
+
>>> from jayron.normalizers import converter
|
|
16
|
+
>>> converter("Bog'chaga bordingmi?")
|
|
17
|
+
'Boğçaga bordingmi?'
|
|
18
|
+
>>> converter.to_old("Boğçaga bordingmi?")
|
|
19
|
+
'Bogʻchaga bordingmi?'
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
import unicodedata
|
|
26
|
+
|
|
27
|
+
from .numbers import OKINA
|
|
28
|
+
from .transliterate import cyrillic_to_latin
|
|
29
|
+
|
|
30
|
+
__all__ = ["APOSTROPHES", "AlphabetConverter", "NEW_LETTERS", "converter", "to_new", "to_old"]
|
|
31
|
+
|
|
32
|
+
# ' ` ´ ʹ ʻ ʼ ʽ ‘ ’ ‛ ′ — all the ways people type the oʻ / gʻ mark
|
|
33
|
+
APOSTROPHES = "['`´ʹʻʼʽ‘’‛′]"
|
|
34
|
+
NEW_LETTERS = "öğşçÖĞŞÇ"
|
|
35
|
+
|
|
36
|
+
_OG = re.compile("([oOgG])" + APOSTROPHES)
|
|
37
|
+
_DIGRAPH = re.compile("([sScC])[hH]")
|
|
38
|
+
_TO_NEW = {"o": "ö", "O": "Ö", "g": "ğ", "G": "Ğ", "s": "ş", "S": "Ş", "c": "ç", "C": "Ç"}
|
|
39
|
+
|
|
40
|
+
# New letters, plus look-alikes from drafts and other keyboards (ō from the 2021 draft, ḡ, ș, ǧ ...).
|
|
41
|
+
_OLD_OG = {"ö": "o", "ō": "o", "ğ": "g", "ḡ": "g", "ǵ": "g", "ǧ": "g"}
|
|
42
|
+
_OLD_OG.update({k.upper(): v.upper() for k, v in list(_OLD_OG.items())})
|
|
43
|
+
_OLD_DIGRAPH = {"ş": "s", "ș": "s", "ç": "c"}
|
|
44
|
+
_OLD_DIGRAPH.update({k.upper(): v.upper() for k, v in list(_OLD_DIGRAPH.items())})
|
|
45
|
+
_NEW_LETTER = re.compile("[" + "".join(_OLD_OG) + "".join(_OLD_DIGRAPH) + "]")
|
|
46
|
+
|
|
47
|
+
# Links and e-mail addresses are left untouched.
|
|
48
|
+
_PROTECTED = re.compile(r"(?:https?://|www\.)\S+|[\w.+-]+@[\w-]+(?:\.[\w-]+)+")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _letters_to_old(text: str, apostrophe: str = OKINA) -> str:
|
|
52
|
+
"""ö -> oʻ, ğ -> gʻ, ş -> sh, ç -> ch (also accepts ō, ḡ, ș, ...)."""
|
|
53
|
+
if text.isascii():
|
|
54
|
+
return text
|
|
55
|
+
|
|
56
|
+
def repl(m: "re.Match[str]") -> str:
|
|
57
|
+
ch = m.group()
|
|
58
|
+
if ch in _OLD_OG:
|
|
59
|
+
return _OLD_OG[ch] + apostrophe
|
|
60
|
+
base = _OLD_DIGRAPH[ch]
|
|
61
|
+
if base.islower():
|
|
62
|
+
return base + "h"
|
|
63
|
+
i, s = m.start(), m.string
|
|
64
|
+
prev = s[i - 1] if i else ""
|
|
65
|
+
nxt = s[i + 1] if i + 1 < len(s) else ""
|
|
66
|
+
all_caps = nxt.isupper() or (prev.isupper() and not nxt.isalpha())
|
|
67
|
+
return base + ("H" if all_caps else "h")
|
|
68
|
+
|
|
69
|
+
return _NEW_LETTER.sub(repl, text)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _letters_to_new(text: str) -> str:
|
|
73
|
+
text = _OG.sub(lambda m: _TO_NEW[m.group(1)], text)
|
|
74
|
+
return _DIGRAPH.sub(lambda m: _TO_NEW[m.group(1)], text)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _outside_links(text: str, fn) -> str:
|
|
78
|
+
parts, last = [], 0
|
|
79
|
+
for m in _PROTECTED.finditer(text):
|
|
80
|
+
parts.append(fn(text[last:m.start()]))
|
|
81
|
+
parts.append(m.group())
|
|
82
|
+
last = m.end()
|
|
83
|
+
parts.append(fn(text[last:]))
|
|
84
|
+
return "".join(parts)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def to_new(text: str, *, transliterate: bool = True) -> str:
|
|
88
|
+
"""Convert to the new alphabet: "Bog'chaga" -> "Boğçaga".
|
|
89
|
+
|
|
90
|
+
Accepts the old Latin alphabet (any apostrophe variant), Cyrillic (when
|
|
91
|
+
``transliterate`` is on), or text already partly in the new alphabet.
|
|
92
|
+
Everything else (case, punctuation, digits, ng, tutuq) is kept as is.
|
|
93
|
+
"""
|
|
94
|
+
if not text:
|
|
95
|
+
return text
|
|
96
|
+
text = unicodedata.normalize("NFC", text)
|
|
97
|
+
if transliterate:
|
|
98
|
+
text = cyrillic_to_latin(text)
|
|
99
|
+
return _outside_links(text, lambda part: _letters_to_new(_letters_to_old(part)))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def to_old(text: str, *, apostrophe: str = OKINA, transliterate: bool = True) -> str:
|
|
103
|
+
"""Convert to the 1995 alphabet: "Boğçaga" -> "Bogʻchaga" (``apostrophe`` marks oʻ/gʻ)."""
|
|
104
|
+
if not text:
|
|
105
|
+
return text
|
|
106
|
+
text = unicodedata.normalize("NFC", text)
|
|
107
|
+
if transliterate:
|
|
108
|
+
text = cyrillic_to_latin(text)
|
|
109
|
+
return _letters_to_old(text, apostrophe)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AlphabetConverter:
|
|
113
|
+
"""Old (1995) <-> new (2026) Uzbek Latin alphabet. Calling it converts to the new one.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
transliterate: also accept Uzbek Cyrillic input.
|
|
117
|
+
apostrophe: character for oʻ/gʻ when converting back to the old alphabet.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, *, transliterate: bool = True, apostrophe: str = OKINA) -> None:
|
|
121
|
+
self.transliterate = transliterate
|
|
122
|
+
self.apostrophe = apostrophe
|
|
123
|
+
|
|
124
|
+
def to_new(self, text: str) -> str:
|
|
125
|
+
return to_new(text, transliterate=self.transliterate)
|
|
126
|
+
|
|
127
|
+
def to_old(self, text: str) -> str:
|
|
128
|
+
return to_old(text, apostrophe=self.apostrophe, transliterate=self.transliterate)
|
|
129
|
+
|
|
130
|
+
def __call__(self, text):
|
|
131
|
+
if isinstance(text, str):
|
|
132
|
+
return self.to_new(text)
|
|
133
|
+
return [self.to_new(t) for t in text]
|
|
134
|
+
|
|
135
|
+
def __repr__(self) -> str:
|
|
136
|
+
return f"AlphabetConverter(transliterate={self.transliterate!r}, apostrophe={self.apostrophe!r})"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
converter = AlphabetConverter()
|