normt2s 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.
- normt2s-0.1.0/LICENSE +21 -0
- normt2s-0.1.0/PKG-INFO +392 -0
- normt2s-0.1.0/README.md +366 -0
- normt2s-0.1.0/pyproject.toml +44 -0
- normt2s-0.1.0/setup.cfg +4 -0
- normt2s-0.1.0/src/normt2s/__init__.py +34 -0
- normt2s-0.1.0/src/normt2s/data/de.json +622 -0
- normt2s-0.1.0/src/normt2s/data/en.json +536 -0
- normt2s-0.1.0/src/normt2s/langs/__init__.py +4 -0
- normt2s-0.1.0/src/normt2s/langs/base.py +82 -0
- normt2s-0.1.0/src/normt2s/langs/de.py +142 -0
- normt2s-0.1.0/src/normt2s/langs/en.py +140 -0
- normt2s-0.1.0/src/normt2s/latex.py +588 -0
- normt2s-0.1.0/src/normt2s/lexicon.py +162 -0
- normt2s-0.1.0/src/normt2s/normalizer.py +371 -0
- normt2s-0.1.0/src/normt2s/stream.py +95 -0
- normt2s-0.1.0/src/normt2s.egg-info/PKG-INFO +392 -0
- normt2s-0.1.0/src/normt2s.egg-info/SOURCES.txt +25 -0
- normt2s-0.1.0/src/normt2s.egg-info/dependency_links.txt +1 -0
- normt2s-0.1.0/src/normt2s.egg-info/requires.txt +3 -0
- normt2s-0.1.0/src/normt2s.egg-info/top_level.txt +1 -0
- normt2s-0.1.0/tests/test_dates.py +55 -0
- normt2s-0.1.0/tests/test_de.py +98 -0
- normt2s-0.1.0/tests/test_en.py +170 -0
- normt2s-0.1.0/tests/test_latex.py +170 -0
- normt2s-0.1.0/tests/test_lexicon.py +125 -0
- normt2s-0.1.0/tests/test_stream.py +58 -0
normt2s-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christian Siegel
|
|
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.
|
normt2s-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: normt2s
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight, dependency-free text normalization (numbers, units, abbreviations) for TTS.
|
|
5
|
+
Author: Christian Siegel
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/c-siegel/normt2s
|
|
8
|
+
Project-URL: Repository, https://github.com/c-siegel/normt2s
|
|
9
|
+
Project-URL: Issues, https://github.com/c-siegel/normt2s/issues
|
|
10
|
+
Keywords: tts,text-normalization,num2words,speech
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# normt2s
|
|
28
|
+
|
|
29
|
+
**Make written text speakable.** `normt2s` turns numbers, units, abbreviations, dates and math into
|
|
30
|
+
the words a text-to-speech engine should say, so `3.5 km/h` is not read as "three dot five k m
|
|
31
|
+
slash h".
|
|
32
|
+
|
|
33
|
+
- **English and German**
|
|
34
|
+
- **No dependencies**, pure Python 3.9+, MIT licensed
|
|
35
|
+
- **Fast**: about 90 µs for a typical sentence
|
|
36
|
+
- **Streaming-ready**: works on LLM output as it arrives, token by token
|
|
37
|
+
- **Easy to extend**: add words with one line of code or a small JSON file
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from normt2s import normalize
|
|
41
|
+
|
|
42
|
+
normalize("It costs $3.50 for 2 kg, e.g. in 1984.")
|
|
43
|
+
# 'It costs three dollars and fifty cents for two kilograms, for example in nineteen eighty-four.'
|
|
44
|
+
|
|
45
|
+
normalize("Der Zug fährt 120 km/h, z. B. um 14:30.", lang="de")
|
|
46
|
+
# 'Der Zug fährt einhundertzwanzig Kilometer pro Stunde, zum Beispiel um vierzehn Uhr dreißig.'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Contents
|
|
50
|
+
|
|
51
|
+
1. [Install](#install)
|
|
52
|
+
2. [What gets converted](#what-gets-converted)
|
|
53
|
+
3. [Adding your own words](#adding-your-own-words)
|
|
54
|
+
4. [Math (LaTeX)](#math-latex)
|
|
55
|
+
5. [Streaming LLM output](#streaming-llm-output)
|
|
56
|
+
6. [Reference: options and API](#reference-options-and-api)
|
|
57
|
+
7. [How it works](#how-it-works)
|
|
58
|
+
8. [Adding a language](#adding-a-language)
|
|
59
|
+
9. [Performance](#performance)
|
|
60
|
+
10. [Known limitations](#known-limitations)
|
|
61
|
+
11. [Development](#development)
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
`normt2s` is available on [PyPI](https://pypi.org/project/normt2s/):
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
pip install normt2s
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
To work on the library itself, install from a clone of the repository instead:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
pip install -e .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## What gets converted
|
|
78
|
+
|
|
79
|
+
Every row is real output from the library.
|
|
80
|
+
|
|
81
|
+
### English
|
|
82
|
+
|
|
83
|
+
| Category | Input | Spoken |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| Numbers | `1,234 points` | one thousand two hundred thirty-four points |
|
|
86
|
+
| | `3.14`, `-5`, `007` | three point one four, minus five, zero zero seven |
|
|
87
|
+
| | `1st`, `23rd` | first, twenty-third |
|
|
88
|
+
| | `50%`, `1/2 cup` | fifty percent, one half cup |
|
|
89
|
+
| Years | `in 1984`, `the 1990s` | in nineteen eighty-four, the nineteen nineties |
|
|
90
|
+
| | `I have 1984 apples` | I have one thousand nine hundred eighty-four apples |
|
|
91
|
+
| Time | `3:45 pm`, `at 14:30` | three forty-five p m, at fourteen thirty |
|
|
92
|
+
| Dates | `March 3rd, 2024`, `2024-03-15` | March third, twenty twenty-four; March fifteenth, twenty twenty-four |
|
|
93
|
+
| Units | `100 km/h`, `20°C`, `9.81 m/s²`, `1 ft` / `2 ft` | one hundred kilometers per hour, twenty degrees Celsius, nine point eight one meters per second squared, one foot / two feet |
|
|
94
|
+
| Currency | `$3.50`, `€5`, `5 EUR`, `$2.5 million` | three dollars and fifty cents, five euros, five euros, two point five million dollars |
|
|
95
|
+
| Abbreviations | `Dr. Smith, e.g. ...`, `etc.` | Doctor Smith, for example ..., et cetera |
|
|
96
|
+
| Math symbols | `2 + 3 = 5`, `x²`, `a ≤ b` | two plus three equals five, x squared, a is less than or equal to b |
|
|
97
|
+
|
|
98
|
+
### German
|
|
99
|
+
|
|
100
|
+
| Category | Input | Spoken |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| Numbers | `1.234,5` | eintausendzweihundertvierunddreißig Komma fünf |
|
|
103
|
+
| | `1/3`, `3/4` | ein drittel, drei viertel |
|
|
104
|
+
| Dates | `15.03.2024` | fünfzehnter März zweitausendvierundzwanzig |
|
|
105
|
+
| | `am 15.03.2024` | am fünfzehnten März zweitausendvierundzwanzig |
|
|
106
|
+
| | `am 3. Mai`, `im Mai 2024` | am dritten Mai, im Mai zweitausendvierundzwanzig |
|
|
107
|
+
| Time | `um 14:30` | um vierzehn Uhr dreißig |
|
|
108
|
+
| Units | `1 kg`, `1 min`, `21 min` | ein Kilogramm, eine Minute, einundzwanzig Minuten |
|
|
109
|
+
| Currency | `3,50 €`, `5 €` | drei Euro fünfzig Cent, fünf Euro |
|
|
110
|
+
| Abbreviations | `z. B.`, `ca. 5 Mio.` | zum Beispiel, circa fünf Millionen |
|
|
111
|
+
|
|
112
|
+
German dates take the right case ending from the word before them: `am` / `vom` / `zum` / `dem`
|
|
113
|
+
/ `den` give *fünfzehnten*, `der` gives *fünfzehnte*, and a date on its own gives *fünfzehnter*.
|
|
114
|
+
|
|
115
|
+
### What is left alone
|
|
116
|
+
|
|
117
|
+
Code (`` `inline` `` and fenced blocks), URLs, e-mail addresses, HTML tags and Markdown link
|
|
118
|
+
targets are never changed:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
normalize("Run `pip install numpy==1.26` and open https://example.com/v1.2/page")
|
|
122
|
+
# unchanged
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Things that only look like numbers are kept too: versions with several dots are read digit by
|
|
126
|
+
digit (`3.2.1` is "three point two point one"), and letters glued to digits (`H2O`, `mp3`) are
|
|
127
|
+
not split.
|
|
128
|
+
|
|
129
|
+
## Adding your own words
|
|
130
|
+
|
|
131
|
+
There is one rule to remember: **create a `Normalizer`, add words to it, and use that instance.**
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from normt2s import Normalizer, normalize
|
|
135
|
+
|
|
136
|
+
n = Normalizer("en")
|
|
137
|
+
n.add_abbreviation("LOLA", "Lola")
|
|
138
|
+
n.add_unit("kn", "knot") # plural defaults to "knots"
|
|
139
|
+
n.add_unit("ch", "child", "children") # or give the plural yourself
|
|
140
|
+
n.add_currency("CHF", ("franc", "francs"), ("rappen", "rappen"))
|
|
141
|
+
|
|
142
|
+
n.normalize("LOLA sails 12 kn with 2 ch for 5.20 CHF")
|
|
143
|
+
# 'Lola sails twelve knots with two children for five francs and twenty rappen'
|
|
144
|
+
|
|
145
|
+
normalize("LOLA sails 12 kn", n) # the helper also accepts your instance
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> **Gotcha:** `normalize(text, "en")` uses a separate, shared instance. Words you add to your own
|
|
149
|
+
> `Normalizer("en")` are **not** visible there. Pass your instance (`normalize(text, n)`), or edit
|
|
150
|
+
> the shared one with `get_normalizer("en")`.
|
|
151
|
+
|
|
152
|
+
Your entries override the built-in ones, and changes apply immediately, even to an instance you
|
|
153
|
+
have already used.
|
|
154
|
+
|
|
155
|
+
### Which method for what
|
|
156
|
+
|
|
157
|
+
| You want to change | Method | Example |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| An abbreviation | `add_abbreviation(key, to)` | `("tbh", "to be honest")` |
|
|
160
|
+
| A unit after a number | `add_unit(symbol, singular, plural, gender)` | `("kn", "knot")` |
|
|
161
|
+
| A currency | `add_currency(symbol, major, minor)` | see above |
|
|
162
|
+
| A math symbol such as `=` or `×` | `add_symbol(key, word)` | `("=", "is")` |
|
|
163
|
+
| A LaTeX command | `lexicon.add_latex_command(name, spoken)` | `("R", "the reals")` |
|
|
164
|
+
| A LaTeX function | `lexicon.add_latex_function(name, spoken)` | `("sinc", "sinc")` |
|
|
165
|
+
| A LaTeX sentence template | `lexicon.add_phrase(key, template)` | `("frac", "{a} divided by {b}")` |
|
|
166
|
+
| Remove an entry | `lexicon.remove(kind, key)` | `("abbreviations", "vs.")` |
|
|
167
|
+
|
|
168
|
+
Notes on abbreviations:
|
|
169
|
+
|
|
170
|
+
- Matching is case sensitive, but for lowercase keys a capitalised variant is added for you
|
|
171
|
+
(`e.g.` also gives `E.g.` → "For example"). Pass `capitalized=False` to turn that off.
|
|
172
|
+
- Spaces inside a key are optional when matching: `z. B.` also matches `z.B.`
|
|
173
|
+
- Set `final=True` for abbreviations that can end a sentence (`etc.`), so the full stop is kept
|
|
174
|
+
when a new sentence follows.
|
|
175
|
+
|
|
176
|
+
### Using a file
|
|
177
|
+
|
|
178
|
+
Put your words in a JSON (or, on Python 3.11+, TOML) file and load it:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
n.load_lexicon("my_terms.json")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"abbreviations": {
|
|
187
|
+
"LMS": "learning management system",
|
|
188
|
+
"etc.": {"to": "et cetera", "final": true}
|
|
189
|
+
},
|
|
190
|
+
"units": {"kn": ["knot"], "ch": ["child", "children"]},
|
|
191
|
+
"currencies": {"CHF": {"major": ["franc", "francs"], "minor": ["rappen", "rappen"]}},
|
|
192
|
+
"symbols": {"=": "equals"},
|
|
193
|
+
"latex": {
|
|
194
|
+
"commands": {"R": "the reals"},
|
|
195
|
+
"functions": {"sinc": "sinc"},
|
|
196
|
+
"phrases": {"frac": "{a} divided by {b}"}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The built-in data uses exactly this format: see [src/normt2s/data/en.json](src/normt2s/data/en.json)
|
|
202
|
+
and [de.json](src/normt2s/data/de.json). A unit entry is `[singular, plural, gender]`, where the
|
|
203
|
+
plural and gender are optional and the gender (`"m"`, `"f"`, `"n"`) only matters in German.
|
|
204
|
+
|
|
205
|
+
## Math (LaTeX)
|
|
206
|
+
|
|
207
|
+
Math in `$...$`, `$$...$$`, `\(...\)` and `\[...\]` is read aloud:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
normalize(r"The area is $\pi r^2$.")
|
|
211
|
+
# 'The area is pi r squared.'
|
|
212
|
+
|
|
213
|
+
normalize(r"Solve $\frac{a+b}{c} = \sqrt{x}$ now")
|
|
214
|
+
# 'Solve the fraction a plus b over c, end fraction equals square root of x now'
|
|
215
|
+
|
|
216
|
+
normalize(r"Es gilt $x \in \mathbb{R}$ und $\sum_{i=1}^{n} i$.", lang="de")
|
|
217
|
+
# 'Es gilt x ist Element von den reellen Zahlen und die Summe von i gleich eins bis n über i.'
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Supported: fractions, roots, powers and subscripts, sums, products, integrals and limits, Greek
|
|
221
|
+
letters, relations and set notation, functions such as `\sin` and `\log`, absolute values and
|
|
222
|
+
norms, accents (`\vec`, `\hat`), `\binom`, factorials, primes, matrices, `cases`, and `\text{}` /
|
|
223
|
+
`\mathrm{}`. Units after a number work too: `5\,\mathrm{km}` is "five kilometers".
|
|
224
|
+
|
|
225
|
+
Good to know:
|
|
226
|
+
|
|
227
|
+
- **Parentheses** are only spoken where they change the meaning: `(a+b)^2` and `2(x+1)` get
|
|
228
|
+
"open paren ... close paren", `y = (x+1)` does not.
|
|
229
|
+
- **Complex arguments** are fenced so the listener can follow them: "the square root of x plus
|
|
230
|
+
one, end root".
|
|
231
|
+
- **Malformed LaTeX** never raises. It is left exactly as it was written.
|
|
232
|
+
- **Dollar signs:** a single `$` only starts math under the pandoc rule, so
|
|
233
|
+
`from $5 to $6` stays currency.
|
|
234
|
+
- **Turn it off** with `Normalizer("en", latex="keep")` to leave all math untouched.
|
|
235
|
+
- **Commands vs. functions:** `add_latex_command` is for symbol-like names (`\cdot`, `\R`),
|
|
236
|
+
`add_latex_function` for names applied to an argument (`\sinc x`). Commands win when both exist,
|
|
237
|
+
and `add_latex_function` warns if a command with that name already exists.
|
|
238
|
+
|
|
239
|
+
## Streaming LLM output
|
|
240
|
+
|
|
241
|
+
When the LLM answers token by token, you do not want to wait for the full answer, and you cannot
|
|
242
|
+
normalize each token alone (`"1"`, `"20"`, `" km"` would be read separately). The stream wrapper
|
|
243
|
+
buffers text and releases it only when it is safe:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
stream = n.stream()
|
|
247
|
+
for token in llm_tokens:
|
|
248
|
+
speakable = stream.feed(token) # "" until a safe boundary is reached
|
|
249
|
+
if speakable:
|
|
250
|
+
tts.say(speakable)
|
|
251
|
+
tts.say(stream.flush()) # call once at the end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
or, as a generator:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
for speakable in n.normalize_stream(llm_tokens):
|
|
258
|
+
tts.say(speakable)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
What that looks like:
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
feed 'The train ' -> ''
|
|
265
|
+
feed 'goes 1' -> ''
|
|
266
|
+
feed '20 km' -> ''
|
|
267
|
+
feed '/h. Dr' -> 'The train goes one hundred twenty kilometers per hour. '
|
|
268
|
+
feed '. Smith ' -> ''
|
|
269
|
+
feed 'pays $3' -> ''
|
|
270
|
+
feed '.50 now' -> ''
|
|
271
|
+
feed '. Done' -> ''
|
|
272
|
+
flush -> 'Doctor Smith pays three dollars and fifty cents now. Done'
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Text is released at the end of a sentence or at a newline. It is never cut inside a number and its
|
|
276
|
+
unit, an abbreviation (`Dr. Smith`), a date (`am 3. Mai`), a code block or a math span. The
|
|
277
|
+
combined output is identical to normalizing the whole text at once, whatever the chunk sizes (this
|
|
278
|
+
is tested). If a long run has no boundary, it is flushed after `max_buffer` characters (default
|
|
279
|
+
400) at a whitespace that does not follow a number.
|
|
280
|
+
|
|
281
|
+
The trade-off: speech starts at the end of the first sentence, not at the first token.
|
|
282
|
+
|
|
283
|
+
## Reference: options and API
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
Normalizer(lang="en", lexicon=None, latex="speak")
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
| Argument | Meaning |
|
|
290
|
+
|---|---|
|
|
291
|
+
| `lang` | `"en"` or `"de"` (or a `Language` object) |
|
|
292
|
+
| `lexicon` | a `Lexicon` to use instead of the built-in data |
|
|
293
|
+
| `latex` | `"speak"` (default) reads math aloud, `"keep"` leaves it untouched |
|
|
294
|
+
|
|
295
|
+
| Call | What it does |
|
|
296
|
+
|---|---|
|
|
297
|
+
| `normalize(text, lang="en")` | one-shot helper; `lang` may be a code or your own `Normalizer` |
|
|
298
|
+
| `get_normalizer(lang)` | the shared instance behind `normalize(text, "en")` |
|
|
299
|
+
| `n.normalize(text)` (or `n(text)`) | normalize a string |
|
|
300
|
+
| `n.stream(max_buffer=400)` | a `StreamNormalizer` with `feed()` and `flush()` |
|
|
301
|
+
| `n.normalize_stream(chunks)` | generator over speakable pieces |
|
|
302
|
+
| `n.add_*`, `n.load_lexicon(path)` | extend the dictionaries (see above) |
|
|
303
|
+
| `n.lexicon` | the underlying `Lexicon` with the LaTeX methods |
|
|
304
|
+
|
|
305
|
+
## How it works
|
|
306
|
+
|
|
307
|
+
Text is split into spans. Code, URLs, e-mails and HTML are kept as they are, math goes to the
|
|
308
|
+
LaTeX speaker, and everything else runs through these steps:
|
|
309
|
+
|
|
310
|
+
1. **Abbreviations** (`Dr.` → "Doctor")
|
|
311
|
+
2. **Dates** (`15.03.2024`, `March 3rd`)
|
|
312
|
+
3. **Symbols that need digits** (`5 - 3`, `x^2`)
|
|
313
|
+
4. **Numbers**, together with what follows them: units, percent, currency, ordinals, am/pm
|
|
314
|
+
5. **Remaining symbols** (`=`, `+`, `²`, `≤`)
|
|
315
|
+
|
|
316
|
+
All regular expressions are compiled once and rebuilt only when the dictionaries change.
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
src/normt2s/
|
|
320
|
+
__init__.py public API: normalize, get_normalizer
|
|
321
|
+
normalizer.py the pipeline above (start reading here)
|
|
322
|
+
lexicon.py the editable dictionaries and how they are loaded
|
|
323
|
+
stream.py buffering for streamed text
|
|
324
|
+
latex.py LaTeX tokenizer, parser and speaker
|
|
325
|
+
langs/ per-language rules: number words, times, dates, fractions
|
|
326
|
+
data/ en.json, de.json: abbreviations, units, currencies, symbols, math vocabulary
|
|
327
|
+
tests/ examples of every rule, a good way to see expected behaviour
|
|
328
|
+
benchmarks/ bench.py
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Rules that need code (how to build "twenty-one") live in `langs/`. Everything that is just a word
|
|
332
|
+
list lives in `data/`.
|
|
333
|
+
|
|
334
|
+
## Adding a language
|
|
335
|
+
|
|
336
|
+
1. Create `src/normt2s/langs/xx.py` with a subclass of `Language`: set the separators and words
|
|
337
|
+
(`thousands_sep`, `decimal_sep`, `minus`, `point`, `percent`, ...) and implement `cardinal`,
|
|
338
|
+
`ordinal`, `time` and `fraction`. Optionally add `year`, `decade`, `date_regex` and
|
|
339
|
+
`format_date`. End the file with `register(YourLanguage())`.
|
|
340
|
+
2. Import it in `langs/__init__.py`.
|
|
341
|
+
3. Create `src/normt2s/data/xx.json` in the format shown above.
|
|
342
|
+
|
|
343
|
+
`langs/en.py` and `langs/de.py` are working examples.
|
|
344
|
+
|
|
345
|
+
## Performance
|
|
346
|
+
|
|
347
|
+
`python benchmarks/bench.py` on a laptop:
|
|
348
|
+
|
|
349
|
+
| Input | Time |
|
|
350
|
+
|---|---|
|
|
351
|
+
| a typical sentence (74 characters) | about 90 µs |
|
|
352
|
+
| a 600-character paragraph | about 0.9 ms |
|
|
353
|
+
| one-time startup (building and compiling) | about 30 ms |
|
|
354
|
+
|
|
355
|
+
## Known limitations
|
|
356
|
+
|
|
357
|
+
- **Acronyms** such as "AI" or "LLM" are not expanded by default. Add the ones you need with
|
|
358
|
+
`add_abbreviation`.
|
|
359
|
+
- **Subtraction:** `-` is only read as "minus" between two numbers with spaces around it
|
|
360
|
+
(`5 - 3`). In `x - 1` it is left as it is. In LaTeX math it is always read.
|
|
361
|
+
- **German ordinal dots** are only recognised in dates (`am 3. Mai`), not in general
|
|
362
|
+
(`der 3. Platz`).
|
|
363
|
+
- **German number + noun:** `1 Stunde` reads "eins Stunde", because the gender of a spelled-out
|
|
364
|
+
noun is unknown. Use the unit symbol (`1 h`) to get "eine Stunde".
|
|
365
|
+
- **Dates:** English `3/15/2024` and `15 March 2024` are not handled, and neither are German
|
|
366
|
+
two-digit years.
|
|
367
|
+
- **Durations** such as `1:30:15` are not recognised.
|
|
368
|
+
- **Single capital-letter units** (`V`, `W`, `L`) only match when attached to the number (`12V`),
|
|
369
|
+
so that "5 A grade" is not read as five amperes.
|
|
370
|
+
- **LaTeX:** a lone `|` reads as "such that", so `P(A|B)` is not "given". Layout commands such as
|
|
371
|
+
`\overset` and `\underbrace` are not interpreted.
|
|
372
|
+
|
|
373
|
+
## Development
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
pip install -e .[dev]
|
|
377
|
+
pytest
|
|
378
|
+
python benchmarks/bench.py
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The tests double as documentation: `tests/test_en.py` and `tests/test_de.py` list the expected
|
|
382
|
+
output for each rule.
|
|
383
|
+
|
|
384
|
+
## AI notice
|
|
385
|
+
|
|
386
|
+
This package was co-authored with Claude Code (Claude Sonnet 5.5, Anthropic). The code, tests and
|
|
387
|
+
documentation were written in collaboration between a human maintainer and the AI assistant.
|
|
388
|
+
Please review the output against your own requirements before relying on it in production.
|
|
389
|
+
|
|
390
|
+
## License
|
|
391
|
+
|
|
392
|
+
MIT, see [LICENSE](LICENSE).
|