Skip to main content

voxnorm

Text normalization for TTS: written forms into spoken forms, multilingual, pure Python. Currently speaks Traditional Chinese (zh-TW), Simplified Chinese (zh-CN), English, Japanese, and Korean.

Language models and databases write for the eye — 12:00, NT$1,200, 0912-345-678 — and a small TTS frontend handed that text verbatim has to guess, and guesses badly. voxnorm rewrites the written forms into what a speaker would actually say, and leaves everything else character-for-character alone:

>>> import voxnorm
>>> voxnorm.normalize("會議在12:00開始")
'會議在十二點開始'
>>> voxnorm.normalize("The meeting starts at 12:00")
"The meeting starts at twelve o'clock"
>>> voxnorm.normalize("会議は12:00からです")      # kana → Japanese
'会議は十二時からです'
>>> voxnorm.normalize("회의는 12:00에 시작합니다")  # hangul → Korean (native-numeral hours)
'회의는 열두 시에 시작합니다'
>>> voxnorm.normalize("電話是0912-345-678。")
'電話是零九一二三四五六七八。'
>>> voxnorm.normalize("NT$1,200", lang="zh")
'新台幣一千兩百元'
>>> voxnorm.normalize("会议在12:00开始")        # simplified glyphs → zh-CN
'会议在十二点开始'

Design

Two layers, split where languages stop sharing:

  • Detection is language-neutral (voxnorm/tokens.py): one scanner finds the spans — times, time ranges and am/pm times, ISO and four-digit-year slash dates, phone numbers (with area codes in parentheses, + country codes, #/轉 extensions and North American 3-3-4 numbers), currency, percentages, temperatures, metric units, room numbers, letter-glued identifiers and masked tails, emails and URLs, decimals, integers — and names their kind. Angle-bracket markup (<break time="500ms"/>) passes through untouched.
  • Verbalisation is per-language (voxnorm/zh.py, cn.py, en.py, ja.py, ko.py): a new language is a new module and one registry entry. English, Japanese and Korean ride on num2words for their number words, each adding the readings the library cannot know — a Japanese year without the era default, Korean native-numeral hours (한 시, never 일 시), the irregular Korean months 유월/시월. Chinese is spelled out in-repo, Traditional first, because the 兩/二 reading choice (一千兩百, never 一千二百 in zh-TW speech) is exactly what general-purpose converters get wrong; the Simplified variant re-spells that one output vocabulary (兆 → 万亿, and ¥ reads as renminbi rather than yen).

Language is decided per call: an explicit hint wins (normalize(text, lang="zh-TW")), otherwise script evidence decides — kana means Japanese, hangul Korean, a Han character Chinese (Simplified when distinctive simplified glyphs appear, Traditional otherwise), else English. (A Japanese sentence written entirely in kanji is script-identical to Chinese and lands on the Chinese reading — pass lang="ja" when you know better.)

The charter is structural only, never guess: a form whose reading is ambiguous does not convert (1/2 the fraction and 8/17 the date are the same slash string, so a slash form converts only with a four-digit year in front, 2026/10/16; 7-11 the shop and 3-5天 the range are the same hyphen string, so no hyphen form does either, unless its shape is an ISO date, a clock-time range or a phone number). What is not converted is simply spoken as written — which is where it started.

Codes the text does not mark

Structure settles most readings: a leading zero, a letter glued on the left (A123456789), a 號 after three or more digits all say "code, digit by digit". What structure cannot settle is vocabulary — that the 4820 after 車牌 or plate is a plate tail, not a quantity. That word list is open-ended and belongs to the caller, so the package takes no guess and instead accepts the words that settle it:

>>> voxnorm.normalize("車牌4820的車款已逾期12天。", code_words=["車牌"])
'車牌四八二零的車款已逾期十二天。'
>>> voxnorm.normalize("請問您4820的車款繳了嗎?", code_words=["的車款"])
'請問您四八二零的車款繳了嗎?'
>>> voxnorm.normalize("plate 4820 is overdue", code_words=["plate"])
'plate four eight two zero is overdue'
>>> voxnorm.normalize("請撥打 1234-5678,交易序號是12345678901。", code_words=["撥打", "序號是"])
'請撥打 一二三四五六七八,交易序號是一二三四五六七八九零一。'

Each word is matched literally, directly before the digit run (whitespace allowed) or directly after it; include any connective in the word itself (末四碼是, not 末四碼). The words are per-call and per-language — the same API carries 車牌, plate, 会員番号 and 계좌. A word also licenses hyphen-joined groups (1234-5678), which without it stay written: the same string is the year range 1990-2000.

Why not an FST toolkit

nemo_text_processing and WeTextProcessing were measured before this package was written. Both depend on pynini, which publishes no aarch64 wheel — on an ARM box they need OpenFst compiled from source and a permanently vendored libfst.so — and both mis-read the very classes this package exists for: phone numbers as quantities, 302號房 as a date, and (WeTextProcessing) force-simplifying every Traditional character in the sentence it touches. The classes a spoken product needs are a bounded set; implementing them directly costs ~1.5 MB of pure Python (num2words included) and stays debuggable.

Install

uv add voxnorm      # or: pip install voxnorm

Development

uv sync
uv run pytest
uv run ruff check .

Apache-2.0.

Metadata

Release files for voxnorm 0.4.0

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

Source distribution (sdist)

Source distribution for voxnorm 0.4.0
File Size Uploaded Provenance
voxnorm-0.4.0.tar.gz 71.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for voxnorm 0.4.0
File Interpreter ABI Platform
voxnorm-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 109.4 kB

Release files / voxnorm-0.4.0.tar.gz

Download URL voxnorm-0.4.0.tar.gz
Size 71.5 kB
Tags Source
SHA-256 checksum
How to use checksums
f5a86df36d98b6ba136b41ec56fb92282cb7f864cae05f768bae3de8836d15e8
BLAKE2b-256 checksum
How to use checksums
0105ff744bc742e86b7c328f89b9de49fb32b5e1bcf494d4b66dcabcf1a64c42
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Release files / voxnorm-0.4.0-py3-none-any.whl

Download URL voxnorm-0.4.0-py3-none-any.whl
Size 37.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
97aceec3c19de2738fefa36e361f37600c520e4d5547232b96cc50f9bd9f4a50
BLAKE2b-256 checksum
How to use checksums
04011595c7724aab8a0210074d20b93eb7930576ac9cfc8695170631ce2094f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Security for voxnorm 0.4.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for voxnorm-0.4.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

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.

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