Skip to main content

Qurʾanic Phonemizer

PyPI version Python versions Website Dataset Paper License

A Grapheme-to-Phoneme converter (G2P) for the Qurʾan (Hafs riwaya), converting text to phoneme sequences with comprehensive support for waqf phonetic effects and tajweed mappings.

Potential use cases:

  • Speech Recognition: Phonetically transcribe recitations, create training data for machine learning systems
  • Text-to-Speech: Develop accurate TTS systems for Qurʾanic Arabic
  • Linguistic & Tajweed Analysis: Study phonological patterns and tajweed rule distributions across the Qurʾan, apply tajweed rule labels and coloring
  • Educational Tools: Build interactive applications for assessing Qur'an and tajweed pronunciation
  • Timing Analysis: Generate word-by-word timestamps for recitations, analyse madd/ghunnah durations

Table of Contents

Phoneme Inventory

The phoneme inventory uses the standard International Phonetic Alphabet (IPA) Arabic phonemes alongside custom phonemes for Tajweed rules, totalling 67 phonemes, rising to 73 when every optional phoneme is enabled.

All phonemes are defined in data/render/ipa.yaml.

Consonants

Letter Phoneme Letter Phoneme Letter Phoneme
ء , أ , إ , ؤ , ئ , ٱ ʔ ز z / zz ف f / ff
ب b / bb س s / ss ق q / qq
ت , ة t / tt ش ʃ / ʃʃ ك k / kk
ث θ / θθ ص / sˤsˤ ل l / ll / lˤlˤ
ج ʒ / ʒʒ ض / dˤdˤ م m
ح ħ / ħħ ط / tˤtˤ ن n
خ x / xx ظ ðˤ / ðˤðˤ ه h / hh
د d / dd ع ʕ / ʕʕ و w / ww
ذ ð / ðð غ ɣ ي , ى j / jj
ر r / rr / / rˤrˤ

Gemination (shaddah) is represented by repeating the phoneme to create new distinct phonemes. There is no gemination for m / n (modelled as tajweed instead), and none for ʔ / ɣ (they do not occur geminated in the Qurʾān).

Vowels

Vowel Phoneme
َ a
ُ u
ِ i
ا , ى a:
و u:
ي , ى i:

is added by the emphatic_fatha optional phoneme; emphatic alef is always aˤ:. e: is added by imala.

Tajweed Phonemes

Rule Phoneme
Iqlab ŋ
Ikhfaa ŋ
Ikhfaa Shafawi ŋ
Idgham bi-Ghunnah ñ / / /
Idgham Shafawi
Ghunnah Mushaddadah ñ /
Qalqala Q
Tafkheem lˤlˤ (Lam in "Allah")
/ rˤrˤ (Raa)

Iqlab and Ikhfaa Shafawi also have a reading, selected as a variant. Heavy Ikhfaa ŋˤ and extended Qalqala QQ are optional phonemes.

Usage

Installation

pip install quranic-phonemizer

Python 3.11 or newer is required.

Quick Start

from quranic_phonemizer import Phonemizer

pm = Phonemizer()
res = pm.phonemize("1:1")
print(res.text())
print(" ".join(res.phonemes()))
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
b i s m i ll a: h i rˤrˤ a ħ m a: n i rˤrˤ a ħ i: m

Phonemizer() is built once and reused; every call to phonemize() returns a fresh PhonemizeResult. Hafs is the only riwaya shipped, and supported_riwayat() lists what a build has.

For a single very large batch such as the whole Quran, pass suspend_gc=True to defer CPython cyclic collection until that call returns. This preserves the result and restores the prior collector state. Because the collector is process-global, reserve this option for bounded batch work with enough memory for the complete result.

Input References

phonemize() accepts a variety of flexible formats to specify which part of the Qurʾān to phonemize:

Format Example Meaning
"1" Entire chapter 1
"1:1" Verse 1 of chapter 1
"1:1:1" Word 1 of verse 1 of chapter 1
"1:1 - 1:4" Verse range: 1:1 through 1:4
"1:1:1 - 1:1:4" Word range: word 1 of 1:1 through word 4 of 1:1
"113 - 114" Chapter range: 113 through 114

Both ends of a range must be the same depth: chapter to chapter, verse to verse, or word to word.

Stops (Waqf)

Words in a request are joined by default, and its last word is always stopped on. Pass stop_signs=[] to stop at Qurʾanic stop signs, and/or stop_refs=[] to stop at specific words:

Stop key Symbol
"preferred_continue" ۖ
"preferred_stop" ۗ
"optional_stop" ۚ
"compulsory_stop" ۘ
"prohibited_stop" ۙ
"either_stop" ۛ
ref = "68:33"
res = pm.phonemize(ref)
print(res.text())
print(" ".join(res.phonemes()))

print(" ".join(pm.phonemize(ref, stop_signs=["preferred_continue"]).phonemes()))
print(" ".join(pm.phonemize(ref, stop_signs=["optional_stop"]).phonemes()))
كَذَٰلِكَ ٱلْعَذَابُ ۖ وَلَعَذَابُ ٱلْـَٔاخِرَةِ أَكْبَرُ ۚ لَوْ كَانُوا۟ يَعْلَمُونَ
k a ð a: l i k a l ʕ a ð a: b u w a l a ʕ a ð a: b u l ʔ a: x i rˤ a t i ʔ a k b a rˤ u l a w k a: n u: j a ʕ l a m u: n
k a ð a: l i k a l ʕ a ð a: b Q w a l a ʕ a ð a: b u l ʔ a: x i rˤ a t i ʔ a k b a rˤ u l a w k a: n u: j a ʕ l a m u: n
k a ð a: l i k a l ʕ a ð a: b u w a l a ʕ a ð a: b u l ʔ a: x i rˤ a t i ʔ a k b a rˤ l a w k a: n u: j a ʕ l a m u: n

The first stop turns ٱلْعَذَابُ into ...b Q, the second drops the final damma of أَكْبَرُ.

To stop at the end of every verse, pass each verse's last word to stop_refs:

res = pm.phonemize("112", stop_refs=["112:1:4", "112:2:2", "112:3:4"])
for word, sounds in zip(res.words, res.phonemes("word")):
    print(word.location, word.text, " ".join(sounds))
112:1:1 قُلْ q u l
112:1:2 هُوَ h u w a
112:1:3 ٱللَّهُ lˤlˤ a: h u
112:1:4 أَحَدٌ ʔ a ħ a d Q
112:2:1 ٱللَّهُ ʔ a lˤlˤ a: h u
112:2:2 ٱلصَّمَدُ sˤsˤ a m a d Q
112:3:1 لَمْ l a m
112:3:2 يَلِدْ j a l i d Q
112:3:3 وَلَمْ w a l a m
112:3:4 يُولَدْ j u: l a d Q
112:4:1 وَلَمْ w a l a m
112:4:2 يَكُن j a k u
112:4:3 لَّهُۥ ll a h u:
112:4:4 كُفُوًا k u f u w a n
112:4:5 أَحَدٌ ʔ a ħ a d Q

Outputs

phonemize() returns a PhonemizeResult. The methods answer the common questions; the arrays under them are the same answer in full detail, and index into each other.

Member Description
ref The resolved reference string
riwayah, script What produced this result
variant Every variant point with the option in force
extra_phonemes The optional phonemes enabled
phonemes(by) Phoneme tokens; by="word" groups them per word
text(which) "source" for the written text, "recited" for the recited text
alignment(text, grouping) Which characters produced which phonemes
respelling(grouping) The written text against the recited text, block by block
words One per word: location, text, whether it is started on or stopped on
units One per letter: the letter, whether it is geminate, and the vowel after it
sounds One per phoneme: its token and the features behind it
rules One per tajweed rule applied: the rule, its source letter and its host letter
glyphs, rendered One per character of the written and the recited text
spellings, attributions, modifiers The edges joining glyphs, units, sounds and rules
schema_version, canon_digest The shape of this result, and a digest of the passage it was built from

Phonemes

phonemes() returns the reading in order. phonemes("word") groups it by word.

res = pm.phonemize("1:1")
for word, sounds in zip(res.words, res.phonemes("word")):
    print(word.location, word.text, " ".join(sounds))
1:1:1 بِسْمِ b i s m i
1:1:2 ٱللَّهِ ll a: h i
1:1:3 ٱلرَّحْمَـٰنِ rˤrˤ a ħ m a: n i
1:1:4 ٱلرَّحِيمِ rˤrˤ a ħ i: m

ٱللَّهِ starts at ll because its hamza wasl is elided when joined. Where a rule merges a letter into the next word, the phoneme belongs to the word that holds it:

res = pm.phonemize("2:5:4-2:5:5")
for word, sounds in zip(res.words, res.phonemes("word")):
    print(word.location, word.text, " ".join(sounds))
2:5:4 مِّن m i
2:5:5 رَّبِّهِمْ rˤrˤ a bb i h i m

Recited Text

text("recited") returns the Arabic text as it is actually recited, with the transforms that starting on and stopping on a word apply.

for ref in ("1:2", "1:6", "2:2:1"):
    res = pm.phonemize(ref)
    print(res.text(), "->", res.text("recited"))
ٱلْحَمْدُ لِلَّهِ رَبِّ ٱلْعَـٰلَمِينَ -> أَلْحَمْدُ لِلَّآهِ رَبِّ لْعَآلَمِىٓنْ
ٱهْدِنَا ٱلصِّرَٰطَ ٱلْمُسْتَقِيمَ -> إِهْدِنَ صِّرَآطَ لْمُسْتَقِىٓمْ
ذَٰلِكَ -> ذَآلِكْ

Alignment

alignment() pairs characters with the phonemes they produced. text selects which text to pair against, "source" or "recited"; grouping is "glyph" for one pairing per character, or "cell" to group each letter with its own vowel marks.

Each pairing carries indices into the result's arrays: glyphs (or rendered), the sounds it owns, shares for sounds it presents but another pairing owns, silent for characters that produced nothing, and rules.

res = pm.phonemize("1:1:2")
for pairing in res.alignment(text="source", grouping="cell"):
    chars = "".join(res.glyphs[g].char for g in pairing.glyphs)
    sounds = [res.sounds[s].token for s in pairing.sounds]
    rules = [res.rules[r].rule.value for r in pairing.rules]
    print(f"{chars!r} {sounds} {rules}")
'ٱ' ['ʔ'] ['hamza_wasl_fatha']
'' ['a'] []
'ل' [] ['lam_shamsiyyah', 'tafkheem']
'لَّ' ['lˤlˤ', 'aˤ:'] ['lam_shamsiyyah', 'madd_arid_lissukun', 'tafkheem']
'هِ' ['h'] ['waqf_diacritic_drop']

A pairing with no characters is a phoneme no letter wrote, such as the helping fatha above; its after field names the pairing it follows. A pairing with no phonemes is a silent letter, such as the lam of the definite article assimilated into the lam after it.

Respelling

respelling() runs both alignments and returns the blocks where the written text and the recited text correspond. Each block holds indices into the two alignments.

res = pm.phonemize("1:2")
source = res.alignment(text="source", grouping="cell")
recited = res.alignment(text="recited", grouping="cell")
for block in res.respelling():
    before = "".join(res.glyphs[g].char for i in block.source for g in source[i].glyphs)
    after = "".join(res.rendered[g].char for i in block.recited for g in recited[i].glyphs)
    print(f"{before!r} -> {after!r}")
'ٱ' -> 'أَ'
'لْ' -> 'لْ'
'حَ' -> 'حَ'
'مْ' -> 'مْ'
'دُ' -> 'دُ'
'لِ' -> 'لِ'
'لَّ' -> 'لَّآ'
'هِ' -> 'هِ'
'رَ' -> 'رَ'
'بِّ' -> 'بِّ'
'ٱ' -> ''
'لْ' -> 'لْ'
'ع' -> 'ع'
'َـٰ' -> 'َآ'
'لَ' -> 'لَ'
'م' -> 'م'
'ِي' -> 'ِىٓ'
'نَ' -> 'نْ'

Tajweed Rules

rules holds every rule the passage applied. source is the letter the rule is about, and host the letter it merges into; only a merger has a host. Both are indices into units. Cross-word rules disappear when stopping, and rules such as qalqala_kubra and madd_arid_lissukun only appear at a stop.

res = pm.phonemize("2:8:3-2:8:4")
print(res.text(), "|", " ".join(res.phonemes()))
for rule in res.rules:
    source = res.units[rule.source].letter.value if rule.source is not None else None
    host = res.units[rule.host].letter.value if rule.host is not None else None
    print(rule.rule.value, source, host)
مَن يَقُولُ | m a j̃ a q u: l
waqf_diacritic_drop lam None
idgham_bi_ghunnah noon ya
madd_arid_lissukun qaf None
tafkheem qaf None

Badal and silah are rules rather than labels. madd_badal names a long vowel on a hamza and madd_silah the length a pronoun haa takes when the word is joined to; the madd beside a silah says how long it is held.

res = pm.phonemize("104:3")
print(res.text())
for rule in res.rules:
    if rule.rule.value in ("madd_silah", "madd_munfasil"):
        print(res.words[res.units[rule.source].word].text, rule.rule.value)
يَحْسَبُ أَنَّ مَالَهُۥٓ أَخْلَدَهُۥ
مَالَهُۥٓ madd_munfasil
مَالَهُۥٓ madd_silah

tajweed_rules("hafs") lists all 43 rule identifiers with their English name, Arabic name, and a one-sentence summary.

from quranic_phonemizer import tajweed_rules

for identifier, english, arabic, summary in tajweed_rules("hafs")[:3]:
    print(identifier, "|", english, "|", arabic, "|", summary)
izhar | Izhar | إظهار | A quiescent noon or tanween keeps its own sound before a throat letter.
ikhfaa | Ikhfaa | إخفاء | A quiescent noon or tanween is hidden as a hum held at the following letter's place.
iqlab | Iqlab | إقلاب | A quiescent noon or tanween becomes a hummed meem before baa.

Optional Phonemes

Five distinctions are not written by default. Pass extra_phonemes to spend a phoneme on any of them.

Name Distinction
emphatic_fatha A short fatha next to an emphatic letter becomes
emphatic_ikhfaa Ikhfaa before an istilaa letter becomes ŋˤ
qalqala_degree Qalqala kubra and akbar become QQ, apart from sughra Q
tashil An eased hamza becomes ʔ̞
imala The inclined vowel becomes e:, otherwise read as i:
for extra in ((), ("emphatic_fatha",)):
    print(" ".join(Phonemizer(extra_phonemes=extra).phonemize("1:1").phonemes()))

for extra in ((), ("qalqala_degree",)):
    print(" ".join(Phonemizer(extra_phonemes=extra).phonemize("111:1:5").phonemes()))

for extra in ((), ("emphatic_ikhfaa",)):
    print(" ".join(Phonemizer(extra_phonemes=extra).phonemize("107:5:3-107:5:4").phonemes()))

for extra in ((), ("imala",)):
    print(" ".join(Phonemizer(extra_phonemes=extra).phonemize("11:41:6").phonemes()))
b i s m i ll a: h i rˤrˤ a ħ m a: n i rˤrˤ a ħ i: m
b i s m i ll a: h i rˤrˤ aˤ ħ m a: n i rˤrˤ aˤ ħ i: m
w a t a bb Q
w a t a bb QQ
ʕ a ŋ sˤ a l a: t i h i m
ʕ a ŋˤ sˤ a l a: t i h i m
m a ʒ Q r i: h a:
m a ʒ Q r e: h a:

Variants

Where Hafs admits more than one reading, available_variants() lists the points and their options, and variants selects one. The result reports back what was in force in its variant attribute.

from quranic_phonemizer import available_variants

print(sorted(available_variants("hafs")))
print(available_variants("hafs")["iqlab_nasal"])
['daaf_haraka', 'ikhfaa_shafawi_nasal', 'iqlab_nasal', 'madd_lazim_tasheel', 'noon_yaseen_wasl', 'raa_alqitr_waqf', 'raa_asr_waqf', 'raa_firq_wasl', 'raa_misr_waqf', 'raa_nuthur_waqf', 'raa_yasr_waqf', 'seen_sad_al_musaytirun', 'seen_sad_bastah', 'seen_sad_bimusaytir', 'seen_sad_yabsut', 'yaa_aatani_waqf']
{'options': ['assimilated', 'bilabial'], 'default': 'assimilated'}

Every variant takes one scalar option name. A grouped variant applies at all of its covered locations:

bilabial = Phonemizer(variants={"iqlab_nasal": "bilabial"})

print(pm.phonemize("2:56:3-2:56:4").text())
print(" ".join(pm.phonemize("2:56:3-2:56:4").phonemes()))
print(" ".join(bilabial.phonemize("2:56:3-2:56:4").phonemes()))
مِّن بَعْدِ
m i ŋ b a ʕ d Q
m i m̃ b a ʕ d Q

The word-specific variants are scalar too. Their IDs name the covered form and the junction restriction where one applies:

heavy = Phonemizer(variants={"raa_yasr_waqf": "heavy"})

print(pm.phonemize("89:4").text())
print(" ".join(pm.phonemize("89:4").phonemes()))
print(" ".join(heavy.phonemize("89:4").phonemes()))
وَٱلَّيْلِ إِذَا يَسْرِ
w a ll a j l i ʔ i ð a: j a s r
w a ll a j l i ʔ i ð a: j a s rˤ

See the Hafs variants catalogue for all IDs, defaults, locations, phoneme effects, and tajweed projections. Imala is not a variant; its rendering is selected through the imala extra phoneme.

Contributing

If you find any issues or have feature suggestions, please feel free to open an issue or submit a pull request.

Future plans include support for other turuq and riwayat.

Credits

The project makes use of the Quranic Universal Library's (QUL) Hafs script.

Citing

If you use this phonemizer in your work, please cite the paper as follows:

@inproceedings{
ibrahim2025quranic,
title={Qur{\textquoteright}anic Phonemizer: Bringing Tajweed-Aware Phonemes to Qur{\textquoteright}anic Machine Learning},
author={Ahmed Ibrahim},
booktitle={5th Muslims in ML Workshop co-located with NeurIPS 2025},
year={2025},
url={https://openreview.net/forum?id=hZt0JK28iV}
}

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

quranic_phonemizer-2.15.tar.gz (469.9 kB view details)

Uploaded Source

Built Distribution

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

quranic_phonemizer-2.15-py3-none-any.whl (526.1 kB view details)

Uploaded Python 3

File details

Details for the file quranic_phonemizer-2.15.tar.gz.

File metadata

  • Download URL: quranic_phonemizer-2.15.tar.gz
  • Upload date:
  • Size: 469.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.7

File hashes

Hashes for quranic_phonemizer-2.15.tar.gz
Algorithm Hash digest
SHA256 5ea5710e04487b6b888946e7eb31f40f422863af5a46d9595528d1bd91fcf2d4
MD5 fe148dcf25613dd16b9ac62726c9d551
BLAKE2b-256 15572e02c18fab9a5c6f8cfe4b91359e834ec314645b29efd38aee902a8b61a0

See more details on using hashes here.

File details

Details for the file quranic_phonemizer-2.15-py3-none-any.whl.

File metadata

File hashes

Hashes for quranic_phonemizer-2.15-py3-none-any.whl
Algorithm Hash digest
SHA256 710871fe658910465f54628ad69412436134dc6c84b20569e8efd8e36233f807
MD5 83fd053b51a0d6daa213220612949bed
BLAKE2b-256 594367f184af0e6fd4212fb49658aaf349a58a95953f9e1c29ea5ac5612c5a58

See more details on using hashes here.

Release history Release notifications | RSS feed

3.0

2 files

2.15.3

2 files

2.15.2

2 files

2.15.1

2 files

This release

2.15 This release

2 files

2.14

2 files

2.13

2 files

2.12

2 files

2.11

2 files

2.9

2 files

2.8

2 files

2.7

2 files

2.6

2 files

2.5

2 files

2.4

2 files

2.3

2 files

2.2

2 files

2.1

2 files

2.0

2 files

1.0.5

2 files

1.0.4

2 files

1.0.1

2 files

1.0.0

1 file

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