donglao-g2p
Fast Vietnamese–English text normalization and grapheme-to-phoneme conversion for TTS.
English · Tiếng Việt
donglao-g2p is a Rust-backed Python package for preparing Vietnamese,
English, and code-switched text for speech synthesis. Language selection is
automatic; input text does not require language tags.
Hôm nay tôi có meeting John.
→ hom1 naj1 toj1 kɔ5 miːtɪŋ dʒɔn.
The project targets Hanoi Vietnamese and broad General American English. The public API and the phoneme output convention are stable from 1.0.0 on; any change to either is a breaking change and gets a major version. Pronunciation itself is a judgement call, so evaluate it on your own speakers and domains before using generated phonemes as training labels.
Why donglao-g2p?
- Vietnamese text normalization and rule-based syllable G2P.
- Automatic sentence-context Vietnamese–English routing, with corpus-frequency priors for ASCII spellings that both languages claim.
- CMUdict-backed English pronunciation with a graphone OOV fallback.
- Compact phonemic output with Vietnamese tone suffixes
1–6. - Custom spoken-form and phoneme lexicons.
- Deterministic, thread-safe pipelines.
- GIL-free parallel batch processing through Rayon.
- Typed Python API, CLI, ABI3 wheels, and evaluation tools.
- Apache-2.0 licensed for open-source and commercial use.
Installation
Python 3.9 or newer is required. Release wheels are built for Linux x86-64 and aarch64 (manylinux2014). They are ABI3 wheels, so one wheel per architecture covers every supported interpreter.
Install the published package with pip:
python -m pip install donglao-g2p
Add it to a uv-managed project:
uv add donglao-g2p
Or install it into a uv-managed virtual environment:
uv venv
uv pip install donglao-g2p
Until a release is published, install a locally built wheel with either tool:
python -m pip install target/wheels/donglao_g2p-*.whl
uv pip install target/wheels/donglao_g2p-*.whl
For development from source with uv:
git clone https://github.com/DongLaoAI/donglao-g2p.git
cd donglao-g2p
uv sync --dev
uv run pytest
The equivalent pip workflow is:
git clone https://github.com/DongLaoAI/donglao-g2p.git
cd donglao-g2p
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip maturin pytest
maturin develop --release --locked
pytest
Quick start
from donglao_g2p import Pipeline
g2p = Pipeline()
print(g2p.normalize("25 kg lúc 12:30"))
# hai mươi lăm ki-lô-gam lúc mười hai giờ ba mươi phút
print(g2p.phonemize("Hôm nay tôi có meeting John."))
# hom1 naj1 toj1 kɔ5 miːtɪŋ dʒɔn.
Create one pipeline per process and reuse it:
g2p = Pipeline(
ensure_terminal=False,
decimal_style="cardinal",
language="auto",
num_threads=None,
)
Pipeline is immutable and safe to share between threads.
API
Normalize text
g2p.normalize("Giá trị là 3,14 kg")
# giá trị là ba phẩy mười bốn ki-lô-gam
g2p.normalize_batch(["25 kg", "12:30"])
Normalization covers numbers, grouped and decimal values, dates, time, currency, measurement units, percentages, ranges, phone numbers, URLs, email, versions, acronyms, Unicode punctuation, and custom spoken forms.
Select a language
Automatic sentence-context routing remains the default:
Pipeline(language="auto")
Force one language when the caller already knows it:
vi = Pipeline(language="vi")
en = Pipeline(language="en")
vi.normalize("20 kg") # hai mươi ki-lô-gam
en.normalize("20 kg") # twenty kilograms
Forced mode applies to the entire input, including normalization and G2P. Do not force a language for code-switched text unless that is intentional. It also bypasses routing entirely, so none of the evidence described below is consulted.
In auto mode the router works per token over the whole sentence, not per
sentence. Evidence, strongest first:
- A Vietnamese diacritic anywhere in the token decides it outright.
- For a bare ASCII spelling that is both a legal Vietnamese syllable and an
English dictionary word, a built-in frequency table decides. Dictionary
membership alone used to hand these to English, which is why
theowas read asθiːoʊandbaas the initialismbiːeɪ. - A sentence already carrying Vietnamese diacritics pulls its remaining
undecided ASCII tokens toward Vietnamese. Capitalized tokens away from the
start of a segment are exempt, so
South Australia LoopandThe Velvet Ropekeep their English reading. - Otherwise the switch cost keeps a token with its neighbours.
Only a full stop ends a routing segment. Commas stay transparent, so a word fenced by them keeps the surrounding context:
g2p.phonemize("phía đông, nam, dãy đồi.", normalize=False)
# fiə5 ɗoŋ1, naːm1, zaj4 ɗoj2. ("nam" stays Vietnamese)
Structured expressions use the lexical context of the input to choose an English or Vietnamese verbalizer. Inputs with no lexical evidence retain the Vietnamese default for compatibility:
I have 20 apples. → I have twenty apples.
Tôi có 20 quả táo. → tôi có hai mươi quả táo.
20 kg → hai mươi ki-lô-gam.
Decimal notation is locale-aware:
3.14 → ba chấm mười bốn
3,14 → ba phẩy mười bốn
0.05 → không chấm không năm
1.234 → một nghìn hai trăm ba mươi tư
12.345,67 → ... phẩy sáu mươi bảy
12,345.67 → ... chấm sáu mươi bảy
Use digit-by-digit fractional reading for technical data:
digits = Pipeline(decimal_style="digits")
digits.normalize("3.14 và 3,14")
# ba chấm một bốn và ba phẩy một bốn
Phonemize
g2p.phonemize("Hôm nay OpenAI có meeting.")
# hom1 naj1 oʊpən eɪ aɪ kɔ5 miːtɪŋ.
Normalization is enabled by default. Disable it only for canonical, pre-normalized input:
g2p.phonemize("hôm nay, tôi có meeting.", normalize=False)
g2p.phonemize_batch(normalized_texts, normalize=False)
When normalize=False, the caller must expand numbers and symbols and use
canonical punctuation.
Process batches
texts = [
"Xin chào.",
"Nice to meet you.",
"Hôm nay có planning.",
]
phones = g2p.phonemize_batch(texts)
Batch methods preserve order and release the Python GIL. For multi-process services, start with approximately:
num_threads = available CPUs / worker processes
Then benchmark inside the actual production CPU quota.
For millions of records, use the bounded-memory iterators instead of building one very large Python list:
for phones in g2p.phonemize_iter(records, batch_size=4096):
write_result(phones)
for normalized in g2p.normalize_iter(records, batch_size=4096):
write_result(normalized)
Production tuning guidelines:
- Create and warm one
Pipelineper process; do not construct it per request. - Prefer batches of roughly 2,000–10,000 short sentences for offline jobs. The default iterator chunk of 4,096 is a practical starting point.
- Aggregate synchronous service requests into short micro-batches when latency permits. Batches below 64 items deliberately avoid Rayon scheduling overhead.
- With multiple process workers, divide the container CPU quota among their
num_threadsvalues to avoid oversubscription. - Use
phonemize(..., normalize=False)only when the upstream text is already canonical; this skips normalization but changes the caller contract.
Inspect language and OOV decisions
analysis = g2p.analyze("Hôm nay OpenAI có planning.")
print(analysis.normalized)
print(analysis.phonemes)
print(analysis.warnings)
for token in analysis.tokens:
print(token.token, token.language, token.source, token.phonemes)
Token languages are vi, en, or punc. Unknown English words produce an
english_oov:<word> warning. Unsupported scripts or symbols produce <unk>
and an unsupported_token:<token> warning instead of disappearing silently.
Add pronunciation overrides
from donglao_g2p import LexiconEntry, Pipeline
g2p = Pipeline(
overrides={
"DongLao": LexiconEntry(
phonemes="dɔŋ1 laːw1",
language="vi",
case_sensitive=True,
),
"canxi": LexiconEntry(
spoken="can-xi",
language="vi",
),
}
)
Explicit phonemes are recommended for people, products, abbreviations, and specialist vocabulary.
Output convention
Vietnamese output is a compact phonemic representation rather than narrow
phonetic IPA. Predictable duration and coarticulation are left to the acoustic
model. The current schema is identified by
donglao_g2p.__phoneme_profile__ == "compact-v2".
Examples:
hôm → hom1
nay → naj1
tôi → toj1
tai → taːj1
tay → taj1
Tone suffixes:
| Suffix | Vietnamese tone |
|---|---|
1 |
ngang |
2 |
huyền |
3 |
hỏi |
4 |
ngã |
5 |
sắc |
6 |
nặng |
English output uses broad General American IPA without lexical stress marks.
OpenAI remains an English token and is pronounced oʊpən eɪ aɪ; use an
override only when a Vietnamese-localized reading is intentional.
Punctuation
Public output uses only two prosodic tokens:
| Token | Function |
|---|---|
, |
intermediate pause |
. |
sentence boundary |
Semicolons, colons, standalone dashes, medial ellipses, question marks, and
exclamation marks become commas. Terminal ellipses become periods.
Terminal punctuation is not added automatically. Set ensure_terminal=True
to append a period when the input has no terminal punctuation.
CLI
donglao-g2p "Hôm nay tôi có meeting John."
donglao-g2p --normalize-only "25 kg lúc 12:30"
donglao-g2p --analyze "Hôm nay có planning."
donglao-g2p --decimal-style digits "3.14"
donglao-g2p --language en "20 kg"
donglao-g2p --no-normalize "hôm nay, tôi có meeting."
donglao-g2p --ensure-terminal "xin chào"
The CLI reads UTF-8 from standard input when text is omitted:
printf 'Xin chào.' | donglao-g2p
Method
Unicode NFC
→ protect structured expressions
→ text normalization
→ punctuation canonicalization
→ sentence-context language routing
→ Vietnamese rules or English dictionary/OOV G2P
→ compact phoneme rendering
Vietnamese rules operate on onset, nucleus, coda, and tone. English dictionary pronunciations are converted from ARPAbet to IPA. A Viterbi decoder selects Vietnamese or English for each token using orthography, syllable validity, dictionary membership, capitalization, neighboring tokens, sentence-level diacritic evidence, and a language switch cost. Routing segments are bounded by full stops only.
Roughly 875 bare ASCII spellings are simultaneously a legal Vietnamese syllable
and a CMUdict entry, and membership alone cannot separate them. src/lang_prior.rs
resolves the 488 of those that a corpus can settle: each cost is a log frequency
ratio measured over 42.5 million Vietnamese and 28.7 million English tokens,
scaled so a single mid-confidence token cannot override a decisive run of the
other language. Vietnamese counts are for the exact surface string and are
deliberately not folded over diacritics — folding conflates đo, đó, độ and
dò into do and drags genuine English toward Vietnamese. The table is
generated and compiled in; the crate ships no runtime data files.
Validation
Run the correctness suite:
cargo test --locked
pytest
.github/workflows/ci.yml runs the same suite on every push, builds the wheel in
a manylinux2014 container, and installs that exact artifact on Python 3.9 and
3.13 to check the ABI3 claim. Tagging v* runs .github/workflows/release.yml,
which repeats those gates and adds cargo audit, a CycloneDX SBOM, SHA256SUMS,
cosign signatures, and publication to PyPI. Bump the version with
scripts/bump-version.sh <version>; it keeps Cargo.toml, pyproject.toml,
Cargo.lock and uv.lock in agreement, which the release workflow verifies
against the tag before building anything.
Run the explicit 50,000-sentence resource benchmark:
python tests/benchmark_batch.py
python tests/benchmark_batch.py --materialize-inputs
python tests/benchmark_batch.py --threads 8 --json > benchmark.json
On an AMD Ryzen Threadripper 9960X with 48 logical CPUs, a repeated 62-character sentence reached approximately 485,000 sentences/s or 30 million characters/s, with about 100 MiB peak RSS. This is a reference measurement, not a portable performance guarantee.
Linguistic release gates require a human-reviewed JSONL corpus:
python evaluation/evaluate.py /path/to/reviewed-evaluation.jsonl
The metadata evaluator measures routing proxies, OOV coverage, invariants, latency, and throughput:
python evaluation/evaluate_metadata.py
For a streaming language|text debug corpus:
python evaluation/evaluate_unique.py debugs/unique.csv
Text-only metadata does not contain gold phonemes and therefore cannot measure true pronunciation accuracy. Cross-system agreement is also not a gold standard.
Known limitations
- Vietnamese pronunciation targets the Hanoi dialect.
- English OOV names and loanwords may require overrides.
- Undiacriticized Vietnamese cannot be read correctly, and no amount of routing
fixes it:
banstands forbàn,bán,bảnandbạn, and the tone is not recoverable from the spelling. Restore diacritics before phonemizing. - Vietnamese loanwords that are not a single legal syllable (
axit,oxy,campuchia) fail the syllable check, never reach the frequency table, and fall through to the English OOV path. Use overrides for the ones you care about. - Ambiguous numbers and abbreviations cannot always be resolved from text.
- English lexical stress is not represented in the public output.
- The two-token punctuation policy does not preserve question or exclamation prosody.
- The package prepares text and phonemes; it does not train or serve a TTS acoustic model.
Contributing
Contributions are welcome. Read CONTRIBUTING.md before opening a pull request. Linguistic changes must include a minimal golden test and identify the intended dialect or pronunciation convention.
Please do not contribute dictionaries or datasets without clear redistribution rights.
Data and attribution
The English dictionary is based on CMUdict 0.7b. CMUdict permits research and
commercial use and requests acknowledgement when redistributed. Attribution is
retained in NOTICE. Exact Rust dependency versions are pinned in Cargo.lock.
License
Copyright 2026 DongLao.
Licensed under the Apache License 2.0. You may use, modify, and distribute this project, including commercially, subject to the license terms and retained notices.
donglao-g2p
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file donglao_g2p-1.0.0.tar.gz.
File metadata
- Download URL: donglao_g2p-1.0.0.tar.gz
- Upload date:
- Size: 1.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c3869e749ce820690c8aeccac971d1c13421f627ebeb1aa7e90e31ba832fab1c
|
|
| MD5 |
f187cfa32764d93a8e1522c3da9a0881
|
|
| BLAKE2b-256 |
1fc88e802ffe7fd7655353cdc32b3f6922abd4c26cc942913f34900ecc35a9e5
|
Provenance
The following attestation bundles were made for donglao_g2p-1.0.0.tar.gz:
Publisher:
release.yml on DongLaoAI/donglao-g2p
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
donglao_g2p-1.0.0.tar.gz -
Subject digest:
c3869e749ce820690c8aeccac971d1c13421f627ebeb1aa7e90e31ba832fab1c - Sigstore transparency entry: 2498122323
- Sigstore integration time:
-
Permalink:
DongLaoAI/donglao-g2p@bae49acb7d4f66a2c17ac149de2af67c90f53dbd -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/DongLaoAI
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bae49acb7d4f66a2c17ac149de2af67c90f53dbd -
Trigger Event:
push
-
Statement type:
File details
Details for the file donglao_g2p-1.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: donglao_g2p-1.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 3.6 MB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
73594c318ff946397f2ac5347541e27c5ba3373f0bdb98e7b22cd5a7d4a5f63a
|
|
| MD5 |
5fcfb42203dde9569301592a20ff4a81
|
|
| BLAKE2b-256 |
66428853a5a736a96c7cfed73c836ed6e0087acc25da229449bb689786de7c7a
|
Provenance
The following attestation bundles were made for donglao_g2p-1.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on DongLaoAI/donglao-g2p
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
donglao_g2p-1.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
73594c318ff946397f2ac5347541e27c5ba3373f0bdb98e7b22cd5a7d4a5f63a - Sigstore transparency entry: 2498122338
- Sigstore integration time:
-
Permalink:
DongLaoAI/donglao-g2p@bae49acb7d4f66a2c17ac149de2af67c90f53dbd -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/DongLaoAI
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bae49acb7d4f66a2c17ac149de2af67c90f53dbd -
Trigger Event:
push
-
Statement type:
File details
Details for the file donglao_g2p-1.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: donglao_g2p-1.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 4.1 MB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bde1c92a1cdb1f06a53257eb3a4e34659856e99d6d81590fc3c7379757c16083
|
|
| MD5 |
603ab10b5b057a927c207f4bc214ced6
|
|
| BLAKE2b-256 |
b6f0cb57860bb5ee856988bf24603b5599bb3d78e17933b73a1161d8d23ca988
|
Provenance
The following attestation bundles were made for donglao_g2p-1.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yml on DongLaoAI/donglao-g2p
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
donglao_g2p-1.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
bde1c92a1cdb1f06a53257eb3a4e34659856e99d6d81590fc3c7379757c16083 - Sigstore transparency entry: 2498122348
- Sigstore integration time:
-
Permalink:
DongLaoAI/donglao-g2p@bae49acb7d4f66a2c17ac149de2af67c90f53dbd -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/DongLaoAI
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bae49acb7d4f66a2c17ac149de2af67c90f53dbd -
Trigger Event:
push
-
Statement type: