This release is a pre-release and may not be stable for production use.
stressonnx
Multi-language word-stress placement ("accentuation") for text, built for
TTS front-ends. Pure onnxruntime + numpy at runtime — no torch, ever.
from stressonnx import stress
stress("старинный замок стоит на горе", "ru") # 'стари́нный за́мок сто́ит на горе́'
stress("дверной замок надёжен", "ru") # 'дверно́й замо́к надёжен'
Same spelling, different word: за́мок is a castle, замо́к is a lock. stressonnx reads the sentence, decides which one you meant, and marks the stressed vowel.
Why word stress?
Word stress (lexical stress) is which syllable of a word is pronounced prominently: English REcord (noun) vs reCORD (verb). A text-to-speech system must know it before it can pick the right sounds — in Russian, unstressed vowels reduce (о sounds like а), so getting the stress wrong changes every vowel in the word, not just the melody.
Whether you need this library depends on how your language writes stress:
| Orthography type | Examples | What you need |
|---|---|---|
| Stress is free/mobile and not written | Russian, Ukrainian, Belarusian, most languages here | A stress model — this library. Nothing in the spelling of замок tells you which syllable to stress; only context does. |
| Stress is predictable by rule | Kazakh/Turkic (final syllable), Armenian, Georgian | A positional rule covers most words — that is the simple backend: a curated exception vocabulary plus a per-language default rule. |
| Stress is already written | Spanish, Greek, Portuguese | You do not need stressonnx — the orthography (accent rules) already encodes it. But you may still need sense disambiguation, see below. |
The Portuguese case (why "stress is written" isn't the end of the story):
Portuguese spelling pins down the stressed syllable, yet pairs like sede
(thirst /ˈsedɨ/ vs headquarters /ˈsɛdɨ/) share spelling and stress position
while differing in vowel quality — resolvable only from meaning. Our
sibling library bifonia solves that
with the same "disambiguate before G2P" idea used here: its
add_extra_diacritics(text) rewrites the homograph with an explicit
open/closed-vowel diacritic (séde/sêde) so any downstream phonemizer gets
it right. Rule of thumb: unwritten stress → stressonnx; written stress but
sense-dependent pronunciation → a bifonia-style diacritic restorer.
Supported languages
| Model id | Languages | What it is | Measured quality |
|---|---|---|---|
ruaccent |
ru (default) |
Homograph-aware 4-model ONNX pipeline (derived from RUAccent, Apache-2.0) | 0.938 word accuracy, 0.820 on homographs |
silero |
uk, be (defaults), ru |
Neural ONNX pipeline exported from silero_stress (MIT); the ru variant also restores е→ё |
ru 0.914 / uk 0.785 / be 0.873 |
simple |
26 languages¹ | Curated vocabulary + per-language positional rule; no neural inference | parity-locked to upstream / sourced rules, see scoreboard |
¹ ru uk be bg mk sl lv hy ka kk ky tt ba cv sah kjh tg udm mdf myv kbd xal az-Latn az-Cyrl uz-Latn uz-Cyrl — Russian, Ukrainian, Belarusian (all
dictionary-path), Bulgarian, Macedonian, Slovene, Latvian, Armenian,
Georgian, Kazakh, Kyrgyz, Tatar, Bashkir, Chuvash, Yakut, Khakas, Tajik,
Udmurt, Moksha, Erzya, Kabardian, Kalmyk, Azerbaijani (both scripts), Uzbek
(both scripts). Every language — including ru/uk/be — has a torch-free,
ONNX-free rule/vocabulary path (model="simple"), so fallback=True always
bottoms out in a model that needs nothing but a small vocabulary file.
Numbers come from the committed, reproducible
benchmark scoreboard (annotated UD-treebank gold;
read its noise-ceiling note before quoting absolutes). Defaults per
language: ru → ruaccent, uk/be → silero, everything else → simple.
Models are hosted on TigreGotico/stressonnx-models and downloaded automatically on first use (see Offline & failure behavior).
Install
pip install stressonnx
Runtime dependencies: onnxruntime, numpy, huggingface_hub, and
tokenizers (used only by the ru ruaccent pipeline). Optional:
razdel— better Russian sentence splitting inside the ruaccent pipeline (pip install razdel); without it the whole input is processed as one span.pip install stressonnx[export]— torch + silero_stress, only for re-exporting models from a checkout (never needed at runtime).
Usage
from stressonnx import StressPipeline, analyze, stress, to_plus_notation
# One-shot function (caches model instances internally)
stress("Привіт світ", "uk") # 'Приві́т сві́т'
stress("Сәлем Қазақстан", "kk") # 'Сәле́м Қазақста́н'
# Pick a specific model, or a capability instead of a model id
stress("красивый город", "ru", model="silero") # 'краси́вый го́род'
stress("красивый город", "ru", model="simple") # 'краси́вый го́род'
stress("красивый город", "ru", prefer="fast") # 'краси́вый го́род' (silero)
# An isolated engine with its own cache and failure policy
pipeline = StressPipeline()
pipeline.stress("замок стоит на горе", "ru") # 'за́мок сто́ит на горе́'
Structured results: analyze()
For TTS pipelines that need to reason about individual words rather than a
marked string, analyze() returns a StressResult: per-word spans with
offsets into the original, untouched input.
result = analyze("замок стоит на горе", "ru")
result.text # 'за́мок сто́ит на горе́' (same as stress())
for w in result.words:
print(w.text, w.start, w.end, w.stressed_index, w.yo_restored)
# замок 0 5 1 False
# стоит 6 11 2 False
# на 12 14 None False
# горе 15 19 2 False
w.stressed_index is the offset of the stressed vowel within the word
(None if the word carries no mark); w.yo_restored is True when the
backend rewrote е→ё inside that word. Because offsets refer to result.original
exactly as passed in, callers never need to re-parse the marked string to
locate a word.
Batches: stress_batch()
from stressonnx import stress_batch
stress_batch(["привет", "мир"], "ru") # ['приве́т', 'мир']
Output notation
All backends emit the combining acute accent (U+0301) after the
stressed vowel — приве́т is п р и в е U+0301 т. For TTS models trained on
the +-before-vowel format:
stress("привет", "ru", notation="plus") # 'прив+ет'
to_plus_notation("приве́т") # 'прив+ет' (handles NFC-composed á too)
ё restoration (Russian)
Both Russian neural backends restore ё that writers commonly type as е:
зеленый → зелё́ный. Genuinely ambiguous ё-homographs (все/всё) are resolved
by ruaccent from context and deliberately left untouched by silero.
Offline & failure behavior
- First call per language downloads models into the standard Hugging Face
cache (
~/.cache/huggingface, relocatable viaHF_HOME). Sizes:ruruaccent ≈ 500 MB, silero ≈ tens of MB,simplelanguages ≈ 1 MB. - Warm-up ahead of serving: call
warm_up(lang)once at startup so no synthesis request ever blocks on a model download; it loads the same cached instance laterstress()calls use. - Fully offline: after a warm run, set
HF_HUB_OFFLINE=1— cached models keep working, network is never touched. - Typed failures:
from stressonnx import stress, UnsupportedLanguageError, ModelDownloadError
try:
out = stress(text, lang, fallback=True) # opt-in: walk ruaccent→silero→simple
except UnsupportedLanguageError as e: # also catchable as ValueError
out = text # e.supported lists valid tags
except ModelDownloadError as e: # names the exact missing HF path
out = text
fallback=True degrades down the quality chain with a logged warning per
hop (it also engages on ModelLoadError — a corrupt cache — not just
failed downloads); the default (False) raises immediately. A failed
(lang, model) pair is not retried for 30 s, so an outage never triggers a
download attempt per call.
Supply-chain pinning: model files are fetched from a commit-pinned
revision of the HF repo (HF_REPO_REVISION in stressonnx/registry.py),
so releases are reproducible and upstream changes never reach users
implicitly.
Thread safety: stress() and the backends use double-checked locking
for lazy loads; calling from multiple threads is supported (onnxruntime
sessions are thread-safe for inference).
Contracts worth knowing
simple/sileroskip words already carrying U+0301;ruaccentstrips and re-derives (wrong input marks get corrected).- Monosyllables:
simple/sileroalways stress them;ruaccentusually leaves bare single-vowel words unmarked. ruaccentnormalizes its input (drops symbols like…, collapses runs of whitespace) — it is not byte-layout-preserving; the other backends are.- Russian hyphenated clitics (
кто́-то,како́й-нибудь,-либо,-таки,-ка) never receive a mark on the particle.
Guarding by writing system
Feeding Cyrillic to the Georgian model (or vice versa) is a silent no-op — the input never matches the model's alphabet. Check first:
from stressonnx import lang_to_script, MODEL_REGISTRY
if lang_to_script(lang) in MODEL_REGISTRY[model_id].input_scripts:
text = stress(text, lang, model=model_id)
Script values ("cyrillic", "latin", "armenian", "georgian") are
plain strings shared with phoonnx's Alphabet enum for direct comparison.
Adding a language
Two paths, both documented step-by-step for newcomers in
export/ADDING_A_LANGUAGE.md:
- You have a stressed wordlist → ship a
simplelanguage: package the vocabulary, declare alphabet/vowels/OOV rule, upload to the HF repo, register the language tag. No training, no torch. - You have (or train) a neural accentor → export it to ONNX with the
scripts in
export/and add a backend entry.
All 20 upstream silero_stress vocabularies are already shipped, and
export/build_wiktionary_vocab.py turns any language whose Wiktionary
headwords carry stress marks into a simple language (that is how
Bulgarian, Macedonian, Slovene, Latvian and Ukrainian were built; Russian
came from the RUAccent pronunciation dictionary).
Documentation
Pick your entry point:
-
New to all of this? The Why word stress? section above, then
docs/languages.md— a plain-language, per-language guide to why stress marking is needed and what we do about it. -
Developer integrating stressonnx? Usage above, then
docs/models.mdfor backend contracts anddocs/architecture.mdfor the package internals. -
Linguist checking our homework?
docs/languages.mdcarries the typology and per-rule citations;benchmarks/RESULTS.mdthe measurements; every OOV rule's source is quoted instressonnx/backends/simple.py. -
docs/languages.md— per-language guide: stress system, why TTS needs it, what stressonnx does, with citations. -
docs/models.md— every backend in depth: pipeline stages, per-language rules, quality numbers, contracts. -
docs/architecture.md— package layout, the single download layer, data flow. -
benchmarks/RESULTS.md— the scoreboard and how to reproduce it. -
examples/— runnable scripts, from basics to homograph demos.
Related projects
- phoonnx — ONNX TTS engine; calls stressonnx before phonemization for Russian voices.
- bifonia — European-Portuguese homograph disambiguation by meaning (the "written stress, unwritten vowel quality" counterpart to this library).
- scriptconv — script detection and phoneme-notation conversion.
- silabificador — Portuguese syllabification and stress by rule.
License
Apache-2.0. Model attributions: RUAccent (Den4ikAI, Apache-2.0), silero_stress (snakers4, MIT).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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 stressonnx-0.0.2a1.tar.gz.
File metadata
- Download URL: stressonnx-0.0.2a1.tar.gz
- Upload date:
- Size: 67.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
435682375b2c175c6563e7fcf9e9898b7a9a232f2bfca994f3ed23c45849c348
|
|
| MD5 |
793feadf8d704d43956a8874586cfb59
|
|
| BLAKE2b-256 |
1603771a992ba9c2ff0053d218bb97912775fbd14a48b36456f65b8bbf031baa
|
File details
Details for the file stressonnx-0.0.2a1-py3-none-any.whl.
File metadata
- Download URL: stressonnx-0.0.2a1-py3-none-any.whl
- Upload date:
- Size: 57.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e622b82dbacc1d6f8c22cbe05e0498f400cdd81ee1b76479577302eb134e5207
|
|
| MD5 |
503cf62733492a3ca062317b75ae02ea
|
|
| BLAKE2b-256 |
119ce88b324c0fbd5ca2347eaa7997c16706d9c20205fa2cb7f48e5b155dcece
|