Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

text2spoken

text2spoken rewrites written text into the words that a person says when they read it aloud. It reads numbers, dates, times, money, measures, percentages, telephone numbers, colours and abbreviations. The rest of the text stays as it is, character for character.

from text2spoken import normalize

normalize("Dr Smith paid £3.50 for 2 tickets on 14 March 2024.", "en-GB")
# 'Doctor Smith paid three pounds and fifty pence for two tickets on the fourteenth of March twenty twenty-four.'

Install

uv pip install --prerelease=allow text2spoken

The library needs Python 3.11 or later. It uses no network and loads no model.

Uses

Text to speech

A voice speaks words, not digits or symbols. Normalise the text before you send it to the voice:

normalize("Meet on 03/04/2025 at 7:30 pm.", "en-US")
# 'Meet on March fourth twenty twenty-five at seven thirty PM.'

The language tag selects the reading. en-US reads 03/04/2025 as month, day, year. en-GB reads it as day, month, year.

Phonemizers

A grapheme-to-phoneme model or a rule set expects words. A digit or a symbol has no pronunciation of its own, so the phonemizer drops it or spells it wrongly. Normalise first, then phonemize:

words = normalize("It is 5 km away.", "en-GB")
# 'It is five kilometres away.'
phonemes = my_phonemizer(words)

Word error rate

An ASR system writes "ten thirty" where a reference transcript writes "10:30". The two texts say the same thing, but a word error rate counts each difference as an error. Normalise the reference and the hypothesis with the same tag and the same params:

ref = normalize("The train leaves at 10:30 on 3 May from platform 2.", "en-GB")
hyp = normalize("the train leaves at ten thirty on the third of may from platform two", "en-GB")
# ref: 'The train leaves at ten thirty on the third of May from platform two.'
# hyp: 'the train leaves at ten thirty on the third of may from platform two'

Text that is already in words does not change. The library does not change case or punctuation, so lowercase both texts and remove punctuation before you score them.

How it reads text

The library finds each written form that it can read, and replaces it with words. It does not guess. A form that it cannot read for sure stays as written, and the library logs a WARNING on the text2spoken logger:

normalize("The run took 1:23:45.", "en-GB")
# 'The run took 1:23:45.'  (a duration, not a clock time)

A second pass changes nothing: normalize(normalize(text, lang), lang) gives the same text as one pass. Output depends only on the text, the tag and the params.

The API

Function Returns
normalize(text, lang, params=None, tokenizer=None) The spoken text
analyze(text, lang, params=None, tokenizer=None) An Analysis with the tokens and the spans that the library found
render(analysis, hints=None) The spoken text of an Analysis
supports(lang) A Support record: the forms, genders and option values of a tag
effective_params(lang, params=None) The params of a call, with the language defaults filled in

lang is a BCP 47 tag, for example en, en-GB or pt-PT. A tag that is not valid raises InvalidLanguageTagError.

Grammatical agreement

In many languages a number agrees with its noun. analyze and render let the caller give that information. hints maps the start offset of a span to Features, for example a grammatical gender:

from text2spoken import analyze, render

analysis = analyze("Gate 7 opens at 9:15.", "en-US")
[(span.start, span.end, span.form) for span in analysis.spans]
# [(5, 6, <Form.CARDINAL: 'cardinal'>), (16, 20, <Form.TIME: 'time'>)]
render(analysis)
# 'Gate seven opens at nine fifteen.'

Params

NormalizationParams is a frozen dataclass. A field that is None takes the default of the language variety.

Field Values
forms The Form values to read. params.only(Form.CARDINAL) and params.without(Form.TIME) give a changed copy
date_order DMY, MDY, YMD
clock 12h, 24h
minute_style number, phrase
decimal_mark The decimal mark of the written numbers
scale short, long
default_gender A FrozenMap of language code to GrammaticalGender
register formal, colloquial
phone_regions ISO 3166 region codes for telephone number plans
lexicon A FrozenMap of language tag to a FrozenMap of written form to spoken words
strict True raises an error where the default mode logs a warning

A lexicon entry reads a form your way:

from text2spoken import FrozenMap, NormalizationParams

params = NormalizationParams(lexicon=FrozenMap({"en": FrozenMap({"NHS": "N H S"})}))
normalize("Ask the NHS today.", "en-GB", params)
# 'Ask the N H S today.'

To read only some forms, select them:

from text2spoken import Form

normalize("Pay $5 on 12/03/2024 for 3 items.", "en-US", NormalizationParams().only(Form.CARDINAL))
# 'Pay $5 on 12/03/2024 for three items.'

Errors and warnings

Case Default mode strict=True
A form that the library cannot read Stays as written, WARNING logged UnreadFormError
A param value that the language cannot produce Language default, ParameterNotHonouredWarning UnsupportedParameterError
A gender that the language does not have UndefinedGrammaticalGender UndefinedGrammaticalGender
A hint at an offset where no span starts HintWarning InvalidHintError
A lexicon value that contains a readable form InvalidLexiconError InvalidLexiconError
A token whose text does not match its offsets TokenizerError TokenizerError

Every error is a subclass of Text2SpokenError, and every warning is a subclass of Text2SpokenWarning.

Your own tokenizer

The tokenizer argument takes a callable (text, lang) that returns a sequence of Token(start, end, text). Give it when your pipeline already splits text into tokens, so that the spans and the context words follow your tokens. With no tokenizer, the library uses its default.

Languages

Each language is one file in text2spoken/lang/, named by its primary language subtag. A language with no file reads cardinal numbers from the CLDR spell-out rules, and keeps all other forms as written.

To see what a tag reads:

python -c "from text2spoken import supports; print(supports('en-US'))"

To print the data of a language file as JSON:

python -m text2spoken.export en

English reads the varieties en-GB and en-US. It does not read Roman numerals, letter codes such as GPS, or codes such as X5 after a name; these stay as written. The test rows in tests/data/en.tsv name their source. The checked_by column shows that no native speaker has checked them.

Development

uv venv
uv pip install --prerelease=allow -e ".[test]"
pytest tests

Licence

Apache-2.0. See LICENSE.

Metadata

Release files for text2spoken 0.0.2a19

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for text2spoken 0.0.2a19
File Size Uploaded Provenance Actions
text2spoken-0.0.2a19.tar.gz 90.3 kB Without provenance Details for text2spoken-0.0.2a19.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for text2spoken 0.0.2a19
File Interpreter ABI Platform Actions
text2spoken-0.0.2a19-py3-none-any.whl Python 3 none any Details for text2spoken-0.0.2a19-py3-none-any.whl

Total release size: 148.9 kB

Release files / text2spoken-0.0.2a19.tar.gz

Download URL text2spoken-0.0.2a19.tar.gz
Size 90.3 kB
Tags Source
SHA-256 checksum
How to use checksums
eea177ee4bf751d9520b77e4c97f549fec16aa253f8675e543d8accb8e0b3c44
BLAKE2b-256 checksum
How to use checksums
c9339666ee6516e6c2668125c78e89a0d3fabdc5ced4ffecd7f94e47679b4702
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / text2spoken-0.0.2a19-py3-none-any.whl

Download URL text2spoken-0.0.2a19-py3-none-any.whl
Size 58.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
35fdbfa282fbee4affe8de4a4ed3125a342f205f3d1c7dd09323a90dbeaad284
BLAKE2b-256 checksum
How to use checksums
e2f1137cd4fd7a1a0a1204738c9f4362331dca1b998725369b3992cde3295a81
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for text2spoken 0.0.2a19

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page