Skip to main content
Pre-release

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 a wrong stress 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. You may still need sense disambiguation, see below.

The Portuguese case (why "stress is written" is not 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 needs stressonnx. Written stress with sense-dependent pronunciation needs 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 download 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 for 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] adds 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. Offsets refer to result.original exactly as passed in, so 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 via HF_HOME). Sizes: ru ruaccent about 500 MB, silero tens of MB, simple languages about 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 later stress() calls use.
  • Fully offline. After a warm run, set HF_HUB_OFFLINE=1. Cached models keep working and the 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 come 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/silero skip words already carrying U+0301. ruaccent strips and re-derives (wrong input marks get corrected).
  • Monosyllables: simple/silero always stress them. ruaccent usually leaves bare single-vowel words unmarked.
  • ruaccent normalizes 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:

  1. You have a stressed wordlist. Ship a simple language: package the vocabulary, declare alphabet/vowels/OOV rule, upload to the HF repo, register the language tag. No training, no torch.
  2. 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? Read Why word stress? 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? Read Usage above, then docs/models.md for backend contracts and docs/architecture.md for the package internals.

  • Linguist checking our homework? docs/languages.md carries the typology and per-rule citations. benchmarks/RESULTS.md carries the measurements. Every OOV rule's source is quoted in stressonnx/backends/simple.py.

  • docs/languages.md: per-language guide to the stress system, why TTS needs it, what stressonnx does, with citations.

  • docs/models.md: every backend in depth, with pipeline stages, per-language rules, quality numbers, and contracts.

  • docs/architecture.md: package layout, the single download layer, and data flow.

  • benchmarks/RESULTS.md: the scoreboard and how to reproduce it.

  • examples/: runnable scripts, from basics to homograph demos.

Related projects

  • phoonnx: an ONNX TTS engine that 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

stressonnx-0.0.3a2.tar.gz (70.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stressonnx-0.0.3a2-py3-none-any.whl (58.0 kB view details)

Uploaded Python 3

File details

Details for the file stressonnx-0.0.3a2.tar.gz.

File metadata

  • Download URL: stressonnx-0.0.3a2.tar.gz
  • Upload date:
  • Size: 70.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for stressonnx-0.0.3a2.tar.gz
Algorithm Hash digest
SHA256 905d3e07afe3d39e2c8dbf4358f4a0972f8794db659e98064fd9829a11903651
MD5 f2a3b5ab6cca8dc6082ccf4e5e1dfd5c
BLAKE2b-256 040fb90121be83d31508f2f06b269a9e5c14d5a7d0dcd66936afda5f1ff319d7

See more details on using hashes here.

File details

Details for the file stressonnx-0.0.3a2-py3-none-any.whl.

File metadata

  • Download URL: stressonnx-0.0.3a2-py3-none-any.whl
  • Upload date:
  • Size: 58.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for stressonnx-0.0.3a2-py3-none-any.whl
Algorithm Hash digest
SHA256 2fc793c280832115991be4376c09d791a283b53a8a573c17ad87e2e2a9eb089c
MD5 b4c70af69f790b0a26b2eb464c719bbe
BLAKE2b-256 d9c81cc9f41a9e9eed9f12f75f95b9a7f0dfe2742d99af7b957b7d40c0cbdc32

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page