maxmelichov/RenikudPlus

Python

7

19 commits

updated Aug 31, 2026

See the code

README

ReNikud Plus — Hebrew Grapheme-to-Phoneme Inference

Convert unvocalized Hebrew text into IPA for TTS, speech technology, and spoken-language research. Trained without any benchmark-derived data; on 126k words of unseen podcast speech it scores 93.3% word accuracy.

Benchmark

G2P benchmark comparison

word accuracyRenikudPlusint8GeminiReNikudPhonikud
OVERALL86.285.984.678.266.4
Gender99.399.377.059.937.5
Min. Stress Pairs90.091.388.080.778.7
Stress Homographs91.190.692.679.878.3
Names80.082.075.367.368.0
Acronyms79.676.379.056.637.5
Slang75.675.071.259.041.7
Penultimate Stress85.486.189.482.160.9
Rare Phonemes58.355.057.641.119.9
Foreign78.177.471.656.836.1
Colloquial51.051.025.254.39.3
ILSpeech-test93.293.196.092.585.5

Install

From PyPI:

pip install 'renikud-plus[cpu]'    # CPU inference
pip install 'renikud-plus[gpu]'    # CUDA inference

Pick the extra that matches the machine. ONNX Runtime is not a hard dependency on purpose: onnxruntime and onnxruntime-gpu are separate distributions that unpack into the same onnxruntime/ directory, so pip installs both happily and whichever it writes last wins. If this package depended on the CPU build, it would silently disable CUDA for anyone who also asked for onnxruntime-gpu — and because pip's install order is topological rather than file order, they could not fix it by reordering their requirements. Nothing errors; sessions just quietly run on CPU.

If you already manage ONNX Runtime yourself, plain pip install renikud-plus leaves your build untouched.

From this repo (no PyPI):

uv sync --extra cpu     # or --extra gpu

Then run the example:

uv run python examples/basic.py

The ONNX weights (~310 MB) download automatically from Hugging Face on first use and are cached locally. No separate hf download step is required.

Usage

from renikud_onnx import G2P

g2p = G2P()  # downloads notmax123/RenikudPlus model.onnx if needed
print(g2p.phonemize("שלום לכולם"))
# → ʃalˈom lekulˈam

The weights are the only file the package needs — there is no sidecar JSON. The vocabularies, per-letter constraints and cascade conditioning all travel inside the .onnx metadata.

Pass a local path if you already have the weights, or name the quantized build to download that one instead:

g2p = G2P("model.onnx")                    # a local file
g2p = G2P(filename="model_int8.onnx")      # 4× smaller, within ~0.3 pt overall

For a gender-conditioned ONNX model, pass speaker and target_speaker as 0 (unknown), 1 (male), or 2 (female):

g2p.phonemize("היא רצה", speaker=2, target_speaker=2)

What runs around the model

  • Exact-MAP cascade decode (default). Consonant, vowel and stress are decoded jointly under E(c,v,s) = log P(c) + log P(v|c) + log P(s|c,v), with per-letter legality and "stress needs a vowel" as hard constraints, so the one-stress-per-word choice can flip the vowel and consonant too. Pass exact_map=False for the old greedy argmax.
  • Force lexiconG2P(..., lexicon={"סבתא": "sˈavta"}) or a TSV path. Off unless passed; wins wherever it matches.
  • Hebrew number front end, from hebrew-num2words (installed as a dependency). Digits never appear in training text, so they are expanded to words first — gender-, construct- and context-aware ("המחיר 1250 שקלים"hameχˈiʁ ʔˈelef matˈajim veχamiʃˈim ʃkalˈim), with clock time, date, year, percent, decimal and identifier readings. number_norm="off" skips it. Its API is re-exported, so from renikud_onnx import normalize_numbers still works.
  • Long inputs. Text past the encoder's 2,046-character window is split on sentence, then comma, then word boundaries and decoded window by window; the pieces are contiguous, so concatenation is lossless. A [א-ת]-in-output backstop warns (or raises, on_hebrew_leak="raise") if raw Hebrew survives into the IPA.

Vowelized (pointed) input

Input may carry niqqud or cantillation, in either of two modes.

niqqud="strip" (default). The marks are dropped before tokenization, so pointed text decodes exactly like the same text unpointed — and, in particular, cannot disambiguate anything:

g2p.phonemize("שָׁלוֹם לְכֻּלָּם")  # → ʃalˈom lekulˈam, identical to the bare form

niqqud="use". The points are read as evidence. Each vowel sign, dagesh qal and shin/sin dot is pinned into the exact-MAP energy as a hard constraint, so the model predicts only what the pointing leaves open:

g2p = G2P(niqqud="use")          # or per call: g2p.phonemize(text, niqqud="use")
g2p.phonemize("סֵפֶר")   # sˈefeʁ   book
g2p.phonemize("סַפָּר")   # sˈapaʁ   barber
g2p.phonemize("סָפַר")   # sˈafaʁ   counted
g2p.phonemize("סִפֵּר")   # sˈipeʁ   told

All four are sˈefeʁ under "strip" — the skeleton ספר cannot tell them apart.

Two signs stay ambiguous on purpose and are left to the model: qamats, since qamats qatan (/o/, as in כָּל → kˈol) is written with the same sign in most pointed text, and shva, since shva na is /e/ while shva nah is nothing. Unmarked letters inside a pointed word are read the way pointed text implies — a bare ו is the consonant (a vowel vav would carry holam or dagesh), a bare י after hiriq or tsere is a mater, a word-final bare א/ה is silent.

Caveats. The mode needs the exact-MAP decode (it raises with exact_map=False). Niqqud does not mark stress, so stress stays the model's own call — and where the pointing overrules its reading of the skeleton, its stress can still reflect the reading it preferred (סַפָּרsˈapaʁ, not sapˈaʁ). A constraint that would leave a letter with no legal reading at all is dropped rather than enforced, so partially or sloppily pointed text degrades to the default decode instead of breaking.

On 24 hand-checked pointed words, "use" gets 24/24 vowels and consonants right against 17/24 for "strip"; including stress, 18/24 against 16/24. On the seven pointed possessive-suffix words from the field report (בְּנֹתָיו, צִדְקֹתָיו, קֳדָשָׁיו, …) it matches the reference reading 7/7.

Niqqud output

vocalize renders the same predictions as pointed Hebrew (niqqud) instead of IPA — for TTS engines that read niqqud natively but ignore phoneme markup. It accepts the same speaker / target_speaker arguments.

print(g2p.vocalize("שלום לכולם"))
# → שַׁלוֹם לֶכּוּלַם

Niqqud has no stress mark, so predicted stress is not represented in this output (it is in phonemize). Diacritization is phonetically faithful but not publication-grade — e.g. shva in clusters is omitted. The force lexicon rewrites IPA strings and so does not apply here.

Citation

@misc{melichov2026renikud,
  title={ReNikud: Audio-Supervised Hebrew Grapheme-to-Phoneme Conversion},
  author={Maxim Melichov and Yakov Kolani and Morris Alper},
  year={2026},
  url={https://arxiv.org/pdf/2606.20179},
}

Contributors

maxmelichov

13 commits

idotr7

6 commits

maxmelichov/RenikudPlus

Python

7

19 commits

updated Aug 31, 2026

See the code

README

ReNikud Plus — Hebrew Grapheme-to-Phoneme Inference

Convert unvocalized Hebrew text into IPA for TTS, speech technology, and spoken-language research. Trained without any benchmark-derived data; on 126k words of unseen podcast speech it scores 93.3% word accuracy.

Benchmark

G2P benchmark comparison

word accuracyRenikudPlusint8GeminiReNikudPhonikud
OVERALL86.285.984.678.266.4
Gender99.399.377.059.937.5
Min. Stress Pairs90.091.388.080.778.7
Stress Homographs91.190.692.679.878.3
Names80.082.075.367.368.0
Acronyms79.676.379.056.637.5
Slang75.675.071.259.041.7
Penultimate Stress85.486.189.482.160.9
Rare Phonemes58.355.057.641.119.9
Foreign78.177.471.656.836.1
Colloquial51.051.025.254.39.3
ILSpeech-test93.293.196.092.585.5

Install

From PyPI:

pip install 'renikud-plus[cpu]'    # CPU inference
pip install 'renikud-plus[gpu]'    # CUDA inference

Pick the extra that matches the machine. ONNX Runtime is not a hard dependency on purpose: onnxruntime and onnxruntime-gpu are separate distributions that unpack into the same onnxruntime/ directory, so pip installs both happily and whichever it writes last wins. If this package depended on the CPU build, it would silently disable CUDA for anyone who also asked for onnxruntime-gpu — and because pip's install order is topological rather than file order, they could not fix it by reordering their requirements. Nothing errors; sessions just quietly run on CPU.

If you already manage ONNX Runtime yourself, plain pip install renikud-plus leaves your build untouched.

From this repo (no PyPI):

uv sync --extra cpu     # or --extra gpu

Then run the example:

uv run python examples/basic.py

The ONNX weights (~310 MB) download automatically from Hugging Face on first use and are cached locally. No separate hf download step is required.

Usage

from renikud_onnx import G2P

g2p = G2P()  # downloads notmax123/RenikudPlus model.onnx if needed
print(g2p.phonemize("שלום לכולם"))
# → ʃalˈom lekulˈam

The weights are the only file the package needs — there is no sidecar JSON. The vocabularies, per-letter constraints and cascade conditioning all travel inside the .onnx metadata.

Pass a local path if you already have the weights, or name the quantized build to download that one instead:

g2p = G2P("model.onnx")                    # a local file
g2p = G2P(filename="model_int8.onnx")      # 4× smaller, within ~0.3 pt overall

For a gender-conditioned ONNX model, pass speaker and target_speaker as 0 (unknown), 1 (male), or 2 (female):

g2p.phonemize("היא רצה", speaker=2, target_speaker=2)

What runs around the model

  • Exact-MAP cascade decode (default). Consonant, vowel and stress are decoded jointly under E(c,v,s) = log P(c) + log P(v|c) + log P(s|c,v), with per-letter legality and "stress needs a vowel" as hard constraints, so the one-stress-per-word choice can flip the vowel and consonant too. Pass exact_map=False for the old greedy argmax.
  • Force lexiconG2P(..., lexicon={"סבתא": "sˈavta"}) or a TSV path. Off unless passed; wins wherever it matches.
  • Hebrew number front end, from hebrew-num2words (installed as a dependency). Digits never appear in training text, so they are expanded to words first — gender-, construct- and context-aware ("המחיר 1250 שקלים"hameχˈiʁ ʔˈelef matˈajim veχamiʃˈim ʃkalˈim), with clock time, date, year, percent, decimal and identifier readings. number_norm="off" skips it. Its API is re-exported, so from renikud_onnx import normalize_numbers still works.
  • Long inputs. Text past the encoder's 2,046-character window is split on sentence, then comma, then word boundaries and decoded window by window; the pieces are contiguous, so concatenation is lossless. A [א-ת]-in-output backstop warns (or raises, on_hebrew_leak="raise") if raw Hebrew survives into the IPA.

Vowelized (pointed) input

Input may carry niqqud or cantillation, in either of two modes.

niqqud="strip" (default). The marks are dropped before tokenization, so pointed text decodes exactly like the same text unpointed — and, in particular, cannot disambiguate anything:

g2p.phonemize("שָׁלוֹם לְכֻּלָּם")  # → ʃalˈom lekulˈam, identical to the bare form

niqqud="use". The points are read as evidence. Each vowel sign, dagesh qal and shin/sin dot is pinned into the exact-MAP energy as a hard constraint, so the model predicts only what the pointing leaves open:

g2p = G2P(niqqud="use")          # or per call: g2p.phonemize(text, niqqud="use")
g2p.phonemize("סֵפֶר")   # sˈefeʁ   book
g2p.phonemize("סַפָּר")   # sˈapaʁ   barber
g2p.phonemize("סָפַר")   # sˈafaʁ   counted
g2p.phonemize("סִפֵּר")   # sˈipeʁ   told

All four are sˈefeʁ under "strip" — the skeleton ספר cannot tell them apart.

Two signs stay ambiguous on purpose and are left to the model: qamats, since qamats qatan (/o/, as in כָּל → kˈol) is written with the same sign in most pointed text, and shva, since shva na is /e/ while shva nah is nothing. Unmarked letters inside a pointed word are read the way pointed text implies — a bare ו is the consonant (a vowel vav would carry holam or dagesh), a bare י after hiriq or tsere is a mater, a word-final bare א/ה is silent.

Caveats. The mode needs the exact-MAP decode (it raises with exact_map=False). Niqqud does not mark stress, so stress stays the model's own call — and where the pointing overrules its reading of the skeleton, its stress can still reflect the reading it preferred (סַפָּרsˈapaʁ, not sapˈaʁ). A constraint that would leave a letter with no legal reading at all is dropped rather than enforced, so partially or sloppily pointed text degrades to the default decode instead of breaking.

On 24 hand-checked pointed words, "use" gets 24/24 vowels and consonants right against 17/24 for "strip"; including stress, 18/24 against 16/24. On the seven pointed possessive-suffix words from the field report (בְּנֹתָיו, צִדְקֹתָיו, קֳדָשָׁיו, …) it matches the reference reading 7/7.

Niqqud output

vocalize renders the same predictions as pointed Hebrew (niqqud) instead of IPA — for TTS engines that read niqqud natively but ignore phoneme markup. It accepts the same speaker / target_speaker arguments.

print(g2p.vocalize("שלום לכולם"))
# → שַׁלוֹם לֶכּוּלַם

Niqqud has no stress mark, so predicted stress is not represented in this output (it is in phonemize). Diacritization is phonetically faithful but not publication-grade — e.g. shva in clusters is omitted. The force lexicon rewrites IPA strings and so does not apply here.

Citation

@misc{melichov2026renikud,
  title={ReNikud: Audio-Supervised Hebrew Grapheme-to-Phoneme Conversion},
  author={Maxim Melichov and Yakov Kolani and Morris Alper},
  year={2026},
  url={https://arxiv.org/pdf/2606.20179},
}

Contributors

maxmelichov

13 commits

idotr7

6 commits

Languages

Python

100.0%