hassan869835 commited on
Commit
ebd846f
·
verified ·
1 Parent(s): d08625f

Upload 29 files

Browse files
Files changed (29) hide show
  1. src/quran_muaalem.egg-info/PKG-INFO +357 -0
  2. src/quran_muaalem.egg-info/SOURCES.txt +26 -0
  3. src/quran_muaalem.egg-info/dependency_links.txt +1 -0
  4. src/quran_muaalem.egg-info/entry_points.txt +2 -0
  5. src/quran_muaalem.egg-info/requires.txt +17 -0
  6. src/quran_muaalem.egg-info/top_level.txt +1 -0
  7. src/quran_muaalem/__init__.py +13 -0
  8. src/quran_muaalem/__pycache__/__init__.cpython-312.pyc +0 -0
  9. src/quran_muaalem/__pycache__/decode.cpython-312.pyc +0 -0
  10. src/quran_muaalem/__pycache__/explain.cpython-312.pyc +0 -0
  11. src/quran_muaalem/__pycache__/explain_gradio.cpython-312.pyc +0 -0
  12. src/quran_muaalem/__pycache__/inference.cpython-312.pyc +0 -0
  13. src/quran_muaalem/__pycache__/muaalem_typing.cpython-312.pyc +0 -0
  14. src/quran_muaalem/decode.py +580 -0
  15. src/quran_muaalem/explain.py +250 -0
  16. src/quran_muaalem/explain_gradio.py +112 -0
  17. src/quran_muaalem/gradio_app.py +401 -0
  18. src/quran_muaalem/inference.py +188 -0
  19. src/quran_muaalem/modeling/__init__.py +0 -0
  20. src/quran_muaalem/modeling/__pycache__/__init__.cpython-312.pyc +0 -0
  21. src/quran_muaalem/modeling/__pycache__/configuration_multi_level_ctc.cpython-312.pyc +0 -0
  22. src/quran_muaalem/modeling/__pycache__/modeling_multi_level_ctc.cpython-312.pyc +0 -0
  23. src/quran_muaalem/modeling/__pycache__/multi_level_tokenizer.cpython-312.pyc +0 -0
  24. src/quran_muaalem/modeling/__pycache__/vocab.cpython-312.pyc +0 -0
  25. src/quran_muaalem/modeling/configuration_multi_level_ctc.py +320 -0
  26. src/quran_muaalem/modeling/modeling_multi_level_ctc.py +148 -0
  27. src/quran_muaalem/modeling/multi_level_tokenizer.py +121 -0
  28. src/quran_muaalem/modeling/vocab.py +67 -0
  29. src/quran_muaalem/muaalem_typing.py +78 -0
src/quran_muaalem.egg-info/PKG-INFO ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: quran-muaalem
3
+ Version: 0.0.3
4
+ Summary: Quran Phonetic Script with addional quarnic utils
5
+ Author-email: Abdullah <abdullahamlyossef@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/obadx/quran-muaalem
8
+ Project-URL: Issues, https://github.com/obadx/quran-muaalem/issues
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: diff-match-patch>=20241021
18
+ Requires-Dist: numpy>=2.2.6
19
+ Requires-Dist: quran-transcript>=0.1.0
20
+ Requires-Dist: rich>=14.1.0
21
+ Requires-Dist: torch>=2.7.0
22
+ Requires-Dist: transformers>=4.55.0
23
+ Provides-Extra: test
24
+ Requires-Dist: librosa>=0.11.0; extra == "test"
25
+ Requires-Dist: numba>=0.61.2; extra == "test"
26
+ Requires-Dist: pytest; extra == "test"
27
+ Provides-Extra: ui
28
+ Requires-Dist: gradio>=5.43.1; extra == "ui"
29
+ Requires-Dist: librosa>=0.11.0; extra == "ui"
30
+ Requires-Dist: numba>=0.61.2; extra == "ui"
31
+ Requires-Dist: moviepy>=2.2.1; extra == "ui"
32
+ Dynamic: license-file
33
+
34
+ # Quran Muaalem
35
+
36
+ <div align="center">
37
+ <strong>بعون الله وتوفيقه لا شريك له نقدم المعلم القرآني الذكي القادر على كشف أخطاء التلاوة والتجويد وصفات الحروف</strong>
38
+
39
+ [![PyPI][pypi-badge]][pypi-url]
40
+ [![Python Versions][python-badge]][python-url]
41
+ [![Hugging Face Model][hf-model-badge]][hf-model-url]
42
+ [![Hugging Face Dataset][hf-dataset-badge]][hf-dataset-url]
43
+ [![Google Colab][colab-badge]][colab-url]
44
+ [![arXiv][arxiv-badge]][arxiv-url]
45
+ [![MIT License][mit-badge]][mit-url]
46
+ [![Discord][discord-badge]][discord-url]
47
+
48
+ </div>
49
+
50
+ [pypi-badge]: https://img.shields.io/pypi/v/quran-muaalem.svg
51
+ [pypi-url]: https://pypi.org/project/quran-muaalem/
52
+ [mit-badge]: https://img.shields.io/github/license/obadx/quran-muaalem.svg
53
+ [mit-url]: https://github.com/obadx/quran-muaalem/blob/main/LICENSE
54
+ [python-badge]: https://img.shields.io/pypi/pyversions/quran-muaalem.svg
55
+ [python-url]: https://pypi.org/project/quran-muaalem/
56
+ [colab-badge]: https://img.shields.io/badge/Google%20Colab-Open%20in%20Colab-F9AB00?logo=google-colab&logoColor=white
57
+ [colab-url]: https://colab.research.google.com/drive/1If0G9NtdXiSRu6PVGtIMvLwxizF2jspn?usp=sharing
58
+ [hf-model-badge]: https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Model-blue
59
+ [hf-model-url]: https://huggingface.co/obadx/muaalem-model-v3_0
60
+ [hf-dataset-badge]: https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Dataset-orange
61
+ [hf-dataset-url]: https://huggingface.co/datasets/obadx/muaalem-annotated-v3
62
+ [arxiv-badge]: https://img.shields.io/badge/arXiv-Paper-<COLOR>.svg
63
+ [arxiv-url]: https://arxiv.org/abs/2509.00094
64
+ [discord-badge]: https://img.shields.io/badge/Discord-Join%20Community-7289da?logo=discord&logoColor=white
65
+ [discord-url]: https://discord.gg/hJWW6fCH
66
+
67
+ <div align="center" style="background-color: #f0f8ff; border-left: 5px solid #4CAF50; padding: 15px; margin: 20px 0; border-radius: 5px;">
68
+ <h3 style="color: #2c3e50; margin-top: 0;">📖 رابط لتجربة المعلم القرآني</h3>
69
+ <p style="margin: 10px 0;">يرجى الضغط على للتجربة:</p>
70
+ <a href="https://662a040e1863a5445c.gradio.live" style="display: inline-block; background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; font-weight: bold; margin: 10px 0;">الرابط</a>
71
+ <p style="background-color: #ffeb3b; padding: 8px; border-radius: 3px; display: inline-block; margin: 10px 0;">
72
+ ⚠️ <strong>تنبيه:</strong> هذا الرابط سينتهي في <span style="color: #d32f2f; font-weight: bold;">27 أغسطس 2025</span>
73
+ </p>
74
+ </div>
75
+
76
+ [![ALT_TEXT](https://img.youtube.com/vi/CsFoznO08-Q/0.jpg)](https://www.youtube.com/watch?v=CsFoznO08-Q)
77
+
78
+
79
+ ## الممزيات
80
+
81
+ * مدرب على الرسم الصوتي للقرآن الكريم: [quran-transcript](https://github.com/obadx/quran-transcript) القادر على كشف أخطاء الحروف والتجويد وصفات الحروف
82
+ * نموذج معقول الحجم 660 MP
83
+ * يحتاج فقط إله 1.5 GB من ذاكرة معالج الرسوميات
84
+ * معمارية مبتكرة: CTC متعدد المستويات
85
+
86
+ ## المعمارية
87
+ معمارية مبتكرة: CTC متعدد المستويات. حيث كل مستوي يتدرب على وجه معين
88
+
89
+ ![multi-lvel-ctc](./assets/figures/mutli-level-ctc.png)
90
+
91
+ ## الخطوات المختصرة للتطوير
92
+
93
+ * تجميع التلاوت القرآنية من القراء المتقنين: [prepare-quran-dataset](https://github.com/obadx/prepare-quran-dataset)
94
+ * تقسيم التلاوت على حسب الوقف وليس الآية باستخدام [المقسم](https://github.com/obadx/recitations-segmenter)
95
+ * الحصو على النص القرآني من المقاطع الصوتية باسخدام [نموذج ترتيل](https://huggingface.co/tarteel-ai/whisper-base-ar-quran)
96
+ * تصحيح النصوص المستخرجة من ترتيل باستخدام [خوارزمية التسميع](https://github.com/obadx/quran-transcript)
97
+ * تحويل الرسم الإملائي للرسم العثماني: [quran-transcript](https://github.com/obadx/quran-transcript)
98
+ * تحويل الرسم العثماني للرسم الصوتي للقرآني الكريم الذي يصف كل قواعد التجويد ما عدا الإشمام: [quran-transcript](https://github.com/obadx/quran-transcript)
99
+ * تدريب النموذج على معمارية [Wav2Vec2BERT](https://huggingface.co/docs/transformers/model_doc/wav2vec2-bert)
100
+
101
+
102
+ ## استخدام النوذج
103
+
104
+
105
+ ### استخدام النموذج عن طريق واجهة gradio
106
+
107
+ قم بتزيل [uv](https://docs.astral.sh/uv/)
108
+
109
+ ```bash
110
+ pip install uv
111
+ ```
112
+ أو
113
+ ```bash
114
+ curl -LsSf https://astral.sh/uv/install.sh | sh
115
+ ```
116
+
117
+ بعد ذلك قم بتنزيل `ffmpeg`
118
+
119
+ ```bash
120
+ sudo apt-get update
121
+ sudo apt-get install -y ffmpeg
122
+ ```
123
+
124
+ أو من خلال `anaconda`
125
+ ```bash
126
+ conda install ffmpeg
127
+ ```
128
+
129
+ قم بتشغيل `gradio` ب command واحد فقط:
130
+ ```bash
131
+ uvx --no-cache --from https://github.com/obadx/quran-muaalem.git[ui] quran-muaalem-ui
132
+ ```
133
+ او
134
+ ```bash
135
+ uvx quran-muaalem[ui] quran-muaalem-ui
136
+ ```
137
+
138
+ ### عن طريق python API
139
+
140
+
141
+ #### Installation
142
+
143
+ First, install the required dependencies:
144
+
145
+ ```bash
146
+ # Install system dependencies
147
+ sudo apt-get install -y ffmpeg libsndfile1 portaudio19-dev
148
+
149
+ # Install Python packages
150
+ pip install quran-muaalem librosa "numba>=0.61.2"
151
+ ```
152
+
153
+ ## Basic Usage Example
154
+
155
+ ```python
156
+ """
157
+ Basic example of using the Quran Muaalem package for phonetic analysis of Quranic recitation.
158
+ """
159
+
160
+ from dataclasses import asdict
161
+ import json
162
+ import logging
163
+
164
+ from quran_transcript import Aya, quran_phonetizer, MoshafAttributes
165
+ import torch
166
+ from librosa.core import load
167
+
168
+ # Import the main Muaalem class (adjust import based on your actual package structure)
169
+ from quran_muaalem import Muaalem
170
+
171
+ # Setup logging to see informative messages
172
+ logging.basicConfig(level=logging.INFO)
173
+
174
+ def analyze_recitation(audio_path):
175
+ """
176
+ Analyze a Quranic recitation audio file using the Muaalem model.
177
+
178
+ Args:
179
+ audio_path (str): Path to the audio file to analyze
180
+ """
181
+ # Configuration
182
+ sampling_rate = 16000 # Must be 16000 Hz
183
+ device = "cuda" if torch.cuda.is_available() else "cpu" # Use GPU if available
184
+
185
+ # Step 1: Prepare the Quranic reference text
186
+ # Get the Uthmani script for a specific verse (Aya 8, Surah 75 in this example)
187
+ uthmani_ref = Aya(8, 75).get_by_imlaey_words(17, 9).uthmani
188
+
189
+ # Step 2: Configure the recitation style (Moshaf attributes)
190
+ moshaf = MoshafAttributes(
191
+ rewaya="hafs", # Recitation style (Hafs is most common)
192
+ madd_monfasel_len=2, # Length of separated elongation
193
+ madd_mottasel_len=4, # Length of connected elongation
194
+ madd_mottasel_waqf=4, # Length of connected elongation when stopping
195
+ madd_aared_len=2, # Length of necessary elongation
196
+ )
197
+ # see: https://github.com/obadx/prepare-quran-dataset?tab=readme-ov-file#moshaf-attributes-docs
198
+
199
+ # Step 3: Convert text to phonetic representation
200
+ # see docs for phnetizer: https://github.com/obadx/quran-transcript
201
+ phonetizer_out = quran_phonetizer(uthmani_ref, moshaf, remove_spaces=True)
202
+
203
+ # Step 4: Initialize the Muaalem model
204
+ muaalem = Muaalem(device=device)
205
+
206
+ # Step 5: Load and prepare the audio
207
+ wave, _ = load(audio_path, sr=sampling_rate, mono=True)
208
+
209
+ # Step 6: Process the audio with the model
210
+ # The model analyzes the phonetic properties of the recitation
211
+ outs = muaalem(
212
+ [wave], # Audio data
213
+ [phonetizer_out], # Phonetic reference
214
+ sampling_rate=sampling_rate
215
+ )
216
+
217
+ # Step 7: Display the results
218
+ for out in outs:
219
+ print("Predicted Phonemes:", out.phonemes.text)
220
+
221
+ # Display detailed phonetic features for each phoneme
222
+ for sifa in out.sifat:
223
+ print(json.dumps(asdict(sifa), indent=2, ensure_ascii=False))
224
+ print("*" * 30)
225
+ print("-" * 40)
226
+
227
+ # Explaining Results
228
+ explain_for_terminal(
229
+ outs[0].phonemes.text,
230
+ phonetizer_out.phonemes,
231
+ outs[0].sifat,
232
+ phonetizer_out.sifat,
233
+ )
234
+
235
+
236
+ if __name__ == "__main__":
237
+ # Replace with the path to your audio file
238
+ audio_path = "./assets/test.wav"
239
+
240
+ try:
241
+ analyze_recitation(audio_path)
242
+ except Exception as e:
243
+ logging.error(f"Error processing audio: {e}")
244
+ ```
245
+
246
+ Output:
247
+
248
+ ```bash
249
+ ءِننننَللَااهَبِكُللِشَيءِنعَلِۦۦمُ۾۾۾بَرَااااءَتُممممِنَللَااهِوَرَسُۥۥلِه
250
+ ┏━━━━━━━━━━┳━━━━━��━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
251
+ ┃ Phonemes ┃ Tafashie ┃ Qalqla ┃ Ghonna ┃ Hams Or Jahr ┃ Safeer ┃ Tikraar ┃ Tafkheem Or Taqeeq ┃ Istitala ┃ Shidda Or Rakhawa ┃ Itbaq ┃
252
+ ┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
253
+ │ ءِ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ shadeed │ monfateh │
254
+ │ ننننَ │ not_motafashie │ not_moqalqal │ maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ between │ monfateh │
255
+ │ للَ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ mofakham │ not_mostateel │ between │ monfateh │
256
+ │ اا │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ mofakham │ not_mostateel │ rikhw │ monfateh │
257
+ │ هَ │ not_motafashie │ not_moqalqal │ not_maghnoon │ hams │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ rikhw │ monfateh │
258
+ │ بِ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ shadeed │ monfateh │
259
+ │ كُ │ not_motafashie │ not_moqalqal │ not_maghnoon │ hams │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ shadeed │ monfateh │
260
+ │ للِ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ between │ monfateh │
261
+ │ شَ │ motafashie │ not_moqalqal │ not_maghnoon │ hams │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ rikhw │ monfateh │
262
+ │ ي │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ rikhw │ monfateh │
263
+ │ ءِ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ shadeed │ monfateh │
264
+ │ ن │ not_motafashie │ not_moqalqal │ maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ between │ monfateh │
265
+ │ عَ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ between │ monfateh │
266
+ │ لِ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ between │ monfateh │
267
+ │ ۦۦ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ rikhw │ monfateh │
268
+ │ مُ │ not_motafashie │ not_moqalqal │ maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ between │ monfateh │
269
+ │ ۾۾۾ │ not_motafashie │ not_moqalqal │ maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ rikhw │ monfateh │
270
+ │ بَ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ shadeed │ monfateh │
271
+ │ رَ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ mokarar │ mofakham │ not_mostateel │ between │ monfateh │
272
+ │ اااا │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ mofakham │ not_mostateel │ rikhw │ monfateh │
273
+ │ ءَ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ shadeed │ monfateh │
274
+ │ تُ │ not_motafashie │ not_moqalqal │ not_maghnoon │ hams │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ shadeed │ monfateh │
275
+ │ ممممِ │ not_motafashie │ not_moqalqal │ maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ between │ monfateh │
276
+ │ نَ │ not_motafashie │ not_moqalqal │ maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ between │ monfateh │
277
+ │ للَ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ mofakham │ not_mostateel │ between │ monfateh │
278
+ │ اا │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ mofakham │ not_mostateel │ rikhw │ monfateh │
279
+ │ هِ │ not_motafashie │ not_moqalqal │ not_maghnoon │ hams │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ rikhw │ monfateh │
280
+ │ وَ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ rikhw │ monfateh │
281
+ │ رَ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ mokarar │ mofakham │ not_mostateel │ between │ monfateh │
282
+ │ سُ │ not_motafashie │ not_moqalqal │ not_maghnoon │ hams │ safeer │ not_mokarar │ moraqaq │ not_mostateel │ rikhw │ monfateh │
283
+ │ ۥۥ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ rikhw │ monfateh │
284
+ │ لِ │ not_motafashie │ not_moqalqal │ not_maghnoon │ jahr │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ between │ monfateh │
285
+ │ ه │ not_motafashie │ not_moqalqal │ not_maghnoon │ hams │ no_safeer │ not_mokarar │ moraqaq │ not_mostateel │ rikhw │ monfateh │
286
+ └──────────┴────────────────┴──────────────┴──────────────┴──────────────┴───────────┴─────────────┴────────────────────┴───────────────┴───────────────────┴──────────┘
287
+ ```
288
+
289
+ ### API Docs
290
+
291
+ ```python
292
+ class Muaalem:
293
+ def __init__(
294
+ self,
295
+ model_name_or_path: str = "obadx/muaalem-model-v3_2",
296
+ device: str = "cpu",
297
+ dtype=torch.bfloat16,
298
+ ):
299
+ """
300
+ Initializing Muallem Model
301
+
302
+ Args:
303
+ model_name_or_path: the huggingface model name or path
304
+ device: the device to run model on
305
+ dtype: the torch dtype. Default is `torch.bfloat16` as the model was trained on
306
+ """
307
+
308
+ @torch.no_grad()
309
+ def __call__(
310
+ self,
311
+ waves: list[list[float] | torch.FloatTensor | NDArray],
312
+ ref_quran_phonetic_script_list: list[QuranPhoneticScriptOutput],
313
+ sampling_rate: int,
314
+ ) -> list[MuaalemOutput]:
315
+ """Infrence Funcion for the Quran Muaalem Project
316
+
317
+ waves: input waves batch , seq_len with different formats described above
318
+ ref_quran_phonetic_script_list (list[QuranPhoneticScriptOutput]): list of the
319
+ phonetized ouput of `quran_transcript.quran_phonetizer` with `remove_space=True`
320
+
321
+ sampleing_rate (int): has to be 16000
322
+
323
+ Returns:
324
+ list[MuaalemOutput]:
325
+ A list of output objects, each containing phoneme predictions and their
326
+ phonetic features (sifat) for a processed input.
327
+
328
+ Each MuaalemOutput contains:
329
+ phonemes (Unit):
330
+ A dataclass representing the predicted phoneme sequence with:
331
+ text (str): Concatenated string of all phonemes.
332
+ probs (Union[torch.FloatTensor, list[float]]):
333
+ Confidence probabilities for each predicted phoneme.
334
+ ids (Union[torch.LongTensor, list[int]]):
335
+ Token IDs corresponding to each phoneme.
336
+
337
+ sifat (list[Sifa]):
338
+ A list of phonetic feature dataclasses (one per phoneme) with the
339
+ following optional properties (each is a SingleUnit or None):
340
+ - phonemes_group (str): the phonemes associated with the `sifa`
341
+ - hams_or_jahr (SingleUnit): either `hams` or `jahr`
342
+ - shidda_or_rakhawa (SingleUnit): either `shadeed`, `between`, or `rikhw`
343
+ - tafkheem_or_taqeeq (SingleUnit): either `mofakham`, `moraqaq`, or `low_mofakham`
344
+ - itbaq (SingleUnit): either `monfateh`, or `motbaq`
345
+ - safeer (SingleUnit): either `safeer`, or `no_safeer`
346
+ - qalqla (SingleUnit): eithr `moqalqal`, or `not_moqalqal`
347
+ - tikraar (SingleUnit): either `mokarar` or `not_mokarar`
348
+ - tafashie (SingleUnit): either `motafashie`, or `not_motafashie`
349
+ - istitala (SingleUnit): either `mostateel`, or `not_mostateel`
350
+ - ghonna (SingleUnit): either `maghnoon`, or `not_maghnoon`
351
+
352
+ Each SingleUnit in Sifa properties contains:
353
+ text (str): The feature's categorical label (e.g., "hams", "shidda").
354
+ prob (float): Confidence probability for this feature.
355
+ idx (int): Identifier for the feature class.
356
+ """
357
+ ```
src/quran_muaalem.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/quran_muaalem/__init__.py
5
+ src/quran_muaalem/decode.py
6
+ src/quran_muaalem/explain.py
7
+ src/quran_muaalem/explain_gradio.py
8
+ src/quran_muaalem/gradio_app.py
9
+ src/quran_muaalem/inference.py
10
+ src/quran_muaalem/muaalem_typing.py
11
+ src/quran_muaalem.egg-info/PKG-INFO
12
+ src/quran_muaalem.egg-info/SOURCES.txt
13
+ src/quran_muaalem.egg-info/dependency_links.txt
14
+ src/quran_muaalem.egg-info/entry_points.txt
15
+ src/quran_muaalem.egg-info/requires.txt
16
+ src/quran_muaalem.egg-info/top_level.txt
17
+ src/quran_muaalem/modeling/__init__.py
18
+ src/quran_muaalem/modeling/configuration_multi_level_ctc.py
19
+ src/quran_muaalem/modeling/modeling_multi_level_ctc.py
20
+ src/quran_muaalem/modeling/multi_level_tokenizer.py
21
+ src/quran_muaalem/modeling/vocab.py
22
+ tests/test_align_phonemes.py
23
+ tests/test_best_match.py
24
+ tests/test_exaplain_modules.py
25
+ tests/test_modules.py
26
+ tests/test_muaalem_infrence.py
src/quran_muaalem.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
src/quran_muaalem.egg-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [console_scripts]
2
+ quran-muaalem-ui = quran_muaalem.gradio_app:main
src/quran_muaalem.egg-info/requires.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff-match-patch>=20241021
2
+ numpy>=2.2.6
3
+ quran-transcript>=0.1.0
4
+ rich>=14.1.0
5
+ torch>=2.7.0
6
+ transformers>=4.55.0
7
+
8
+ [test]
9
+ librosa>=0.11.0
10
+ numba>=0.61.2
11
+ pytest
12
+
13
+ [ui]
14
+ gradio>=5.43.1
15
+ librosa>=0.11.0
16
+ numba>=0.61.2
17
+ moviepy>=2.2.1
src/quran_muaalem.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ quran_muaalem
src/quran_muaalem/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .inference import Muaalem
2
+ from .muaalem_typing import MuaalemOutput, Unit, Sifa, SingleUnit
3
+ from .explain import explain_for_terminal
4
+
5
+
6
+ __all__ = [
7
+ "Muaalem",
8
+ "MuaalemOutput",
9
+ "Unit",
10
+ "Sifa",
11
+ "SingleUnit",
12
+ "explain_for_terminal",
13
+ ]
src/quran_muaalem/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (428 Bytes). View file
 
src/quran_muaalem/__pycache__/decode.cpython-312.pyc ADDED
Binary file (14.1 kB). View file
 
src/quran_muaalem/__pycache__/explain.cpython-312.pyc ADDED
Binary file (9.66 kB). View file
 
src/quran_muaalem/__pycache__/explain_gradio.cpython-312.pyc ADDED
Binary file (3.91 kB). View file
 
src/quran_muaalem/__pycache__/inference.cpython-312.pyc ADDED
Binary file (8.92 kB). View file
 
src/quran_muaalem/__pycache__/muaalem_typing.cpython-312.pyc ADDED
Binary file (3.55 kB). View file
 
src/quran_muaalem/decode.py ADDED
@@ -0,0 +1,580 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Sequence, Any
3
+ from dataclasses import dataclass
4
+ import torch
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+ from .modeling.vocab import PAD_TOKEN_IDX
9
+ from .muaalem_typing import Unit
10
+
11
+
12
+ # def align_predicted_sequence(
13
+ # ref: Sequence[Any], predicted: Sequence[Any]
14
+ # ) -> Sequence[Any]:
15
+ # """Aligns the preficeted sequence to the ref sequnce
16
+ #
17
+ # Example (1): `predicted` length > `ref` length
18
+ # ref: abcde
19
+ # predicted: abcdef
20
+ # Returns: abcde
21
+ #
22
+ # Example (2): `predicted` length <`ref` length
23
+ # ref: abcde
24
+ # predicted: abcd
25
+ # Returns: abcde
26
+ #
27
+ # Returns:
28
+ # Sequnce[Any]: new precicted sequence that best matches the ref sequence
29
+ # """
30
+ # n = len(ref)
31
+ # m = len(predicted)
32
+ # if n == m:
33
+ # return predicted
34
+ # if n == 0:
35
+ # return []
36
+ # if m == 0:
37
+ # return ref
38
+ #
39
+ # dp = [[0] * (m + 1) for _ in range(n + 1)]
40
+ #
41
+ # for i in range(1, n + 1):
42
+ # dp[i][0] = 0
43
+ # for j in range(1, m + 1):
44
+ # dp[0][j] = 0
45
+ #
46
+ # for i in range(1, n + 1):
47
+ # for j in range(1, m + 1):
48
+ # insertion = dp[i - 1][j]
49
+ # deletion = dp[i][j - 1]
50
+ # match_cost = dp[i - 1][j - 1] + (1 if ref[i - 1] != predicted[j - 1] else 0)
51
+ # dp[i][j] = min(insertion, deletion, match_cost)
52
+ #
53
+ # i, j = n, m
54
+ # output_chars = []
55
+ # while i > 0 or j > 0:
56
+ # if (
57
+ # i > 0
58
+ # and j > 0
59
+ # and ref[i - 1] == predicted[j - 1]
60
+ # and dp[i][j] == dp[i - 1][j - 1]
61
+ # ):
62
+ # output_chars.append(predicted[j - 1])
63
+ # i -= 1
64
+ # j -= 1
65
+ # elif i > 0 and dp[i][j] == dp[i - 1][j]:
66
+ # output_chars.append(ref[i - 1])
67
+ # i -= 1
68
+ # elif j > 0 and dp[i][j] == dp[i][j - 1]:
69
+ # j -= 1
70
+ # else:
71
+ # output_chars.append(predicted[j - 1])
72
+ # i -= 1
73
+ # j -= 1
74
+ #
75
+ # # return "".join(output_chars[::-1])
76
+ # return output_chars[::-1]
77
+
78
+
79
+ # def align_predicted_sequence(ref, predicted):
80
+ # n = len(ref)
81
+ # m = len(predicted)
82
+ # if m == n:
83
+ # return predicted
84
+ #
85
+ # INF = 10**9
86
+ # dp = [[0] * (m + 1) for _ in range(n + 1)]
87
+ # choice = [[0] * (m + 1) for _ in range(n + 1)]
88
+ #
89
+ # for j in range(m + 1):
90
+ # dp[0][j] = 0
91
+ #
92
+ # for i in range(1, n + 1):
93
+ # dp[i][0] = INF
94
+ #
95
+ # for i in range(1, n + 1):
96
+ # for j in range(1, m + 1):
97
+ # # above
98
+ # option1 = dp[i][j - 1]
99
+ # # adjacent
100
+ # if j >= i:
101
+ # cost = 0 if predicted[j - 1] == ref[i - 1] else 1
102
+ # option2 = dp[i - 1][j - 1] + cost
103
+ # else:
104
+ # option2 = INF
105
+ #
106
+ # if option2 <= option1:
107
+ # dp[i][j] = option2
108
+ # choice[i][j] = 1
109
+ # else:
110
+ # dp[i][j] = option1
111
+ # choice[i][j] = 0
112
+ #
113
+ # print(np.array(dp))
114
+ # print(np.array(choice))
115
+ #
116
+ # res_chars = []
117
+ # i, j = n, m
118
+ # while i > 0 and j > 0:
119
+ # if choice[i][j] == 1:
120
+ # res_chars.append(predicted[j - 1])
121
+ # i -= 1
122
+ # j -= 1
123
+ # else:
124
+ # j -= 1
125
+ #
126
+ # return res_chars[::-1]
127
+
128
+
129
+ def align_chunked_phonemes_sequence(
130
+ ref: list[list[str]],
131
+ predicted: list[list[str]],
132
+ ) -> list[bool]:
133
+ """Aligns phonemes level to get mask that descripts what is missing
134
+
135
+ Returns the mask for the `ref` inputs that best matches the `predicted`
136
+ Note element wise comparison but retuns mask for best seqence (even with errors)
137
+
138
+ Example (1): `predicted` length > `ref` length
139
+ ref: abcde
140
+ predicted: abcdef
141
+ Returns: [T, T, T, T]
142
+
143
+ Example (2): `predicted` length <`ref` length
144
+ ref: abcde
145
+ predicted: abcd
146
+ Returns: [T, T, T, T, F]
147
+
148
+ Example (2): `predicted` length <`ref` length
149
+ ref: afcde
150
+ predicted: abcd
151
+ Returns: [T, T, T, T, F]
152
+
153
+
154
+ Len(mask] == Len(ref)
155
+
156
+ """
157
+
158
+ n = len(predicted)
159
+ m = len(ref)
160
+
161
+ if len(predicted) == len(ref):
162
+ return [True] * len(predicted)
163
+
164
+ if m == 0:
165
+ raise ValueError("`ref` length must not be zero length")
166
+
167
+ dp = [[0] * (m + 1) for _ in range(n + 1)]
168
+ choice = [[0] * (m + 1) for _ in range(n + 1)]
169
+
170
+ for j in range(m + 1):
171
+ dp[0][j] = 0
172
+
173
+ for i in range(1, n + 1):
174
+ dp[i][0] = i
175
+
176
+ for i in range(1, n + 1):
177
+ for j in range(1, m + 1):
178
+ option1 = dp[i][j - 1]
179
+ option2 = dp[i - 1][j] + 1
180
+ cost = 0 if predicted[i - 1][0] == ref[j - 1][0] else 1
181
+ option3 = dp[i - 1][j - 1] + cost
182
+
183
+ if option3 <= option1 and option3 <= option2:
184
+ dp[i][j] = option3
185
+ choice[i][j] = 3
186
+ elif option1 <= option2:
187
+ dp[i][j] = option1
188
+ choice[i][j] = 1
189
+ else:
190
+ dp[i][j] = option2
191
+ choice[i][j] = 2
192
+
193
+ i = n
194
+ j = m
195
+ mask = []
196
+ # res_chars = []
197
+ while i > 0 or j > 0:
198
+ if i > 0 and j > 0:
199
+ if choice[i][j] == 3:
200
+ # res_chars.append(ref[j - 1])
201
+ mask.append(True)
202
+ i -= 1
203
+ j -= 1
204
+ elif choice[i][j] == 2:
205
+ # res_chars.append(missing_placeholder)
206
+ i -= 1
207
+ else:
208
+ j -= 1
209
+ mask.append(False)
210
+ elif i > 0:
211
+ # res_chars.append(missing_placeholder)
212
+ i -= 1
213
+ else:
214
+ j -= 1
215
+ mask.append(False)
216
+
217
+ return mask[::-1]
218
+
219
+
220
+ def align_predicted_sequence(
221
+ ref: Sequence[Any] | torch.LongTensor,
222
+ predicted: Sequence[Any] | torch.LongTensor,
223
+ missing_placeholder=-100,
224
+ ) -> Sequence[Any] | torch.LongTensor:
225
+ """Aligns the preficeted sequence to the ref sequnce
226
+
227
+ Example (1): `predicted` length > `ref` length
228
+ ref: abcde
229
+ predicted: abcdef
230
+ Returns: abcde
231
+
232
+ Example (2): `predicted` length <`ref` length
233
+ ref: abcde
234
+ predicted: abcd
235
+ Returns: abcd(missing_placeholder)
236
+
237
+ Returns:
238
+ Sequnce[Any]: new precicted sequence that best matches the ref sequence
239
+ """
240
+
241
+ n = len(ref)
242
+ m = len(predicted)
243
+
244
+ if len(ref) == len(predicted):
245
+ return predicted, [True] * len(ref)
246
+
247
+ if m == 0:
248
+ return [missing_placeholder] * n
249
+
250
+ dp = [[0] * (m + 1) for _ in range(n + 1)]
251
+ choice = [[0] * (m + 1) for _ in range(n + 1)]
252
+
253
+ for j in range(m + 1):
254
+ dp[0][j] = 0
255
+
256
+ for i in range(1, n + 1):
257
+ dp[i][0] = i
258
+
259
+ for i in range(1, n + 1):
260
+ for j in range(1, m + 1):
261
+ option1 = dp[i][j - 1]
262
+ option2 = dp[i - 1][j] + 1
263
+ cost = 0 if ref[i - 1] == predicted[j - 1] else 1
264
+ option3 = dp[i - 1][j - 1] + cost
265
+
266
+ if option3 <= option1 and option3 <= option2:
267
+ dp[i][j] = option3
268
+ choice[i][j] = 3
269
+ elif option1 <= option2:
270
+ dp[i][j] = option1
271
+ choice[i][j] = 1
272
+ else:
273
+ dp[i][j] = option2
274
+ choice[i][j] = 2
275
+
276
+ i = n
277
+ j = m
278
+ mask = []
279
+ res_chars = []
280
+ while i > 0 or j > 0:
281
+ if i > 0 and j > 0:
282
+ if choice[i][j] == 3:
283
+ res_chars.append(predicted[j - 1])
284
+ mask.append(True)
285
+ i -= 1
286
+ j -= 1
287
+ elif choice[i][j] == 2:
288
+ res_chars.append(missing_placeholder)
289
+ i -= 1
290
+ else:
291
+ j -= 1
292
+ mask.append(False)
293
+ elif i > 0:
294
+ res_chars.append(missing_placeholder)
295
+ i -= 1
296
+ else:
297
+ j -= 1
298
+ mask.append(False)
299
+
300
+ return res_chars[::-1], mask[::-1]
301
+
302
+
303
+ @dataclass
304
+ class CTCDecodeOut:
305
+ """
306
+ Both are 1D Tensors
307
+ """
308
+
309
+ ids: torch.LongTensor
310
+ p: torch.FloatTensor
311
+
312
+
313
+ def ctc_decode(
314
+ batch_ids: torch.LongTensor,
315
+ batch_probs: torch.FloatTensor,
316
+ blank_id=PAD_TOKEN_IDX,
317
+ collapse_consecutive=True,
318
+ ) -> list[CTCDecodeOut]:
319
+ """
320
+ batch_ids (torch.LongTensor): batch on integer ids of shape: batch, sequecne_len
321
+ batch_probs (torch.LongTensor): batch on float32 ids of shape: batch, sequecne_len
322
+
323
+ Return:
324
+ list[tuple[list[int], float]]]:
325
+
326
+
327
+ """
328
+ outs = []
329
+ assert batch_ids.shape == batch_probs.shape
330
+ for seq_idx, seq in enumerate(batch_ids):
331
+ if collapse_consecutive:
332
+ tokens = []
333
+ probs = []
334
+ start = 0
335
+ end = 0
336
+ if len(seq) == 1 and seq[0] != blank_id:
337
+ tokens.append(seq[0])
338
+ probs.append(batch_probs[seq_idx][0])
339
+
340
+ for idx in range(len(seq) - 1):
341
+ curr = seq[idx]
342
+ next = seq[idx + 1]
343
+ # Last Item
344
+ if idx == len(seq) - 2 and curr != blank_id:
345
+ if curr == next:
346
+ end = idx + 2
347
+ tokens.append(curr)
348
+ probs.append(
349
+ batch_probs[seq_idx][start:end].sum() / (end - start)
350
+ )
351
+ elif curr != next:
352
+ end = idx + 1
353
+ tokens.append(curr)
354
+ probs.append(
355
+ batch_probs[seq_idx][start:end].sum() / (end - start)
356
+ )
357
+ tokens.append(next)
358
+ probs.append(batch_probs[seq_idx][idx + 1])
359
+ # Normal Case
360
+ elif curr != next and curr != blank_id:
361
+ end = idx + 1
362
+ tokens.append(curr)
363
+ probs.append(batch_probs[seq_idx][start:end].sum() / (end - start))
364
+ start = end
365
+ elif curr == blank_id:
366
+ start = idx + 1
367
+
368
+ outs.append(
369
+ CTCDecodeOut(
370
+ ids=torch.LongTensor(tokens),
371
+ p=torch.FloatTensor(probs),
372
+ )
373
+ )
374
+ else:
375
+ mask = seq != blank_id
376
+ tokens = seq[mask]
377
+ probs = batch_probs[seq_idx][mask]
378
+ outs.append(CTCDecodeOut(ids=tokens, p=probs))
379
+ return outs
380
+
381
+
382
+ # def multilevel_greedy_decode(
383
+ # level_to_probs: dict[str, torch.FloatTensor],
384
+ # level_to_id_to_vocab: dict[str, dict[int, str]],
385
+ # level_to_ref_ids: dict[str, torch.LongTensor],
386
+ # missing_placeholder=-100,
387
+ # pad_idx=PAD_TOKEN_IDX,
388
+ # ) -> dict[str, list[Unit]]:
389
+ # level_to_units = {}
390
+ # for level in level_to_probs:
391
+ # batch_probs, batch_ids = level_to_probs[level].topk(1, dim=-1)
392
+ # decode_outs = ctc_decode(
393
+ # batch_ids.squeeze(-1), batch_probs.squeeze(-1), collapse_consecutive=True
394
+ # )
395
+ # level_to_units[level] = []
396
+ # for seq_idx, decode_out in enumerate(decode_outs):
397
+ # # Trying to align Ids of the sifat levels
398
+ # if level != "phonemes":
399
+ # aligned_ids, mask = align_predicted_sequence(
400
+ # level_to_ref_ids[level][seq_idx],
401
+ # decode_out.ids,
402
+ # missing_placeholder=missing_placeholder,
403
+ # )
404
+ # else:
405
+ # aligned_ids = decode_out.ids
406
+ #
407
+ # probs = decode_out.p
408
+ # if len(aligned_ids) != len(decode_out.ids):
409
+ # aligned_ids = torch.LongTensor(aligned_ids)
410
+ # mask = torch.BoolTensor(mask)
411
+ #
412
+ # new_probs = torch.zeros(len(aligned_ids), dtype=torch.float32)
413
+ # new_probs[aligned_ids != missing_placeholder] = probs[mask]
414
+ #
415
+ # aligned_ids[aligned_ids == missing_placeholder] = pad_idx
416
+ # probs = new_probs
417
+ #
418
+ # probs = decode_out.p.clone()
419
+ # text = ""
420
+ # for idx in aligned_ids:
421
+ # text += level_to_id_to_vocab[level][int(idx)]
422
+ # level_to_units[level].append(
423
+ # Unit(
424
+ # text=text,
425
+ # probs=probs,
426
+ # ids=aligned_ids,
427
+ # ),
428
+ # )
429
+ #
430
+ # return level_to_units
431
+
432
+
433
+ def phonemes_level_greedy_decode(
434
+ probs: torch.FloatTensor,
435
+ phonemes_level_vocab: dict[int, str],
436
+ ) -> list[Unit]:
437
+ """Decodes only phonemes level
438
+
439
+ Args:
440
+ probs (torch.FloatTensor) of shape batch, seq_len, num_classes
441
+ phonmes_level_vocab (dict[int, str]): mapping ids of phonemes to the
442
+ acutial string represnetation
443
+ """
444
+ batch_probs, batch_ids = probs.topk(1, dim=-1)
445
+ decode_outs = ctc_decode(
446
+ batch_ids.squeeze(-1), batch_probs.squeeze(-1), collapse_consecutive=True
447
+ )
448
+ units = []
449
+ for seq_idx, decode_out in enumerate(decode_outs):
450
+ text = ""
451
+ for idx in decode_out.ids:
452
+ text += phonemes_level_vocab[int(idx)]
453
+ units.append(
454
+ Unit(
455
+ text=text,
456
+ probs=decode_out.p,
457
+ ids=decode_out.ids,
458
+ ),
459
+ )
460
+ return units
461
+
462
+
463
+ def multilevel_greedy_decode(
464
+ level_to_probs: dict[str, torch.FloatTensor],
465
+ level_to_id_to_vocab: dict[str, dict[int, str]],
466
+ level_to_ref_ids: dict[str, torch.LongTensor],
467
+ chunked_phonemes_batch: list[list[str]],
468
+ ref_chuncked_phonemes_batch: list[list[str]],
469
+ phonemes_units: list[Unit],
470
+ missing_placeholder=-100,
471
+ pad_idx=PAD_TOKEN_IDX,
472
+ ) -> dict[str, list[Unit]]:
473
+ level_to_units = {}
474
+ for level in level_to_probs:
475
+ if level == "phonemes":
476
+ continue
477
+ batch_probs, batch_ids = level_to_probs[level].topk(1, dim=-1)
478
+ decode_outs = ctc_decode(
479
+ batch_ids.squeeze(-1), batch_probs.squeeze(-1), collapse_consecutive=True
480
+ )
481
+ level_to_units[level] = []
482
+ for seq_idx, decode_out in enumerate(decode_outs):
483
+ # Trying to align Ids of the sifat levels
484
+ phonemes_mask = align_chunked_phonemes_sequence(
485
+ ref=ref_chuncked_phonemes_batch[seq_idx],
486
+ predicted=chunked_phonemes_batch[seq_idx],
487
+ )
488
+ phonemes_mask = torch.BoolTensor(phonemes_mask)
489
+
490
+ # NOTE:
491
+ # We want to align every level with predited phonme, but
492
+ # in some cases the length of every sifa level is > or < the
493
+ # length for the predited phonemes
494
+ # we slove this by two steps
495
+ # 1. Align the sifa level with length mismatch to the refrence sifa level
496
+ # 2. align the alinged sifa level back to the the length of prediced phonmes
497
+ if len(decode_out.ids) != len(chunked_phonemes_batch[seq_idx]) and (
498
+ len(chunked_phonemes_batch[seq_idx])
499
+ <= len(ref_chuncked_phonemes_batch[seq_idx])
500
+ ):
501
+ logging.info(f"Sequence: `{seq_idx}` has mismatch Level: {level}")
502
+ # 1. Align sifa level to the reference sifa level
503
+ ref_aligned_ids, mask = align_predicted_sequence(
504
+ level_to_ref_ids[level][seq_idx],
505
+ decode_out.ids,
506
+ missing_placeholder=missing_placeholder,
507
+ )
508
+
509
+ probs = decode_out.p
510
+ ref_aligned_ids = torch.LongTensor(ref_aligned_ids)
511
+ mask = torch.BoolTensor(mask)
512
+
513
+ new_probs = torch.zeros(len(ref_aligned_ids), dtype=torch.float32)
514
+ new_probs[ref_aligned_ids != missing_placeholder] = probs[mask]
515
+
516
+ ref_aligned_ids[ref_aligned_ids == missing_placeholder] = pad_idx
517
+
518
+ # 2. Align the predicted aligned to the ref back to the predicted seqence
519
+ aligned_ids = ref_aligned_ids[phonemes_mask]
520
+ new_probs = ref_aligned_ids[phonemes_mask]
521
+
522
+ probs = new_probs
523
+ else:
524
+ aligned_ids = decode_out.ids
525
+ probs = decode_out.p
526
+
527
+ text = ""
528
+ for idx in aligned_ids:
529
+ text += level_to_id_to_vocab[level][int(idx)]
530
+ level_to_units[level].append(
531
+ Unit(
532
+ text=text,
533
+ probs=probs,
534
+ ids=aligned_ids,
535
+ ),
536
+ )
537
+ level_to_units["phonemes"] = phonemes_units
538
+
539
+ return level_to_units
540
+
541
+
542
+ def align_sequence(
543
+ seq: Sequence[int] | torch.LongTensor, target_len: int, min_repeat: int = 3
544
+ ) -> list[int]:
545
+ """Aligns a sequence by removing items from the longest repateted items
546
+
547
+ Returns:
548
+ list[int]: the ids which are goning to be deleted if longest_repeat > len(seq) - target_len
549
+
550
+ Example:
551
+ seq = [1, 0, 1, 0, 0, 0, 0, 1], target_len = 7
552
+ ^ ^ ^
553
+ Longest Repeat ^ ^ ^
554
+ Ouput: [3]
555
+ """
556
+
557
+ if len(seq) <= target_len:
558
+ return []
559
+
560
+ longest_start = 0
561
+ longest_repeat = 0
562
+ curr_repeat = 1
563
+ curr_start = 0
564
+ for idx in range(len(seq) - 1):
565
+ curr = seq[idx]
566
+ next = seq[idx + 1]
567
+ if curr == next:
568
+ curr_repeat += 1
569
+ if (curr != next) or (idx == len(seq) - 2):
570
+ if curr_repeat > longest_repeat and curr_repeat >= min_repeat:
571
+ longest_repeat = curr_repeat
572
+ longest_start = curr_start
573
+ curr_start = idx + 1
574
+ curr_repeat = 1
575
+
576
+ # logical case to remote only from the longest repeat
577
+ if longest_repeat > len(seq) - target_len:
578
+ return list(range(longest_start, longest_start + len(seq) - target_len))
579
+ else:
580
+ return []
src/quran_muaalem/explain.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, asdict
2
+ from typing import Literal
3
+ import json
4
+
5
+ from quran_transcript import SifaOutput
6
+ import quran_transcript.alphabet as alph
7
+ import diff_match_patch as dmp
8
+ from rich import print
9
+ from rich.text import Text
10
+ from rich.table import Table
11
+ from rich.console import Console
12
+
13
+ from .muaalem_typing import Sifa
14
+ from .modeling.vocab import SIFAT_ATTR_TO_ARABIC_WITHOUT_BRACKETS
15
+
16
+
17
+ @dataclass
18
+ class PhonemeGroup:
19
+ ref: str = ""
20
+ out: str = ""
21
+ ref_idx: int | None = None
22
+ out_idx: int | None = None
23
+ tag: Literal["exact", "partial", "insert", "delete"] | None = None
24
+
25
+ def get_tag(self):
26
+ if self.ref == "" and self.out == "":
27
+ raise ValueError("The Entire group is empty")
28
+ if self.ref == self.out:
29
+ self.tag = "exact"
30
+ elif self.ref != "" and self.out == "":
31
+ self.tag = "delete"
32
+ elif self.out != "" and self.ref == "":
33
+ self.tag = "insert"
34
+ else:
35
+ self.tag = "partial"
36
+ return self.tag
37
+
38
+
39
+ def merge_same_phoneme_group(ph_groups: list[PhonemeGroup]) -> list[PhonemeGroup]:
40
+ outs = [ph_groups[0]]
41
+ prev_idx = 0
42
+ for curr_idx in range(1, len(ph_groups)):
43
+ # out is part of ref
44
+ if (
45
+ ph_groups[prev_idx].out_idx is not None
46
+ and ph_groups[curr_idx].ref_idx is not None
47
+ and ph_groups[prev_idx].out in ph_groups[curr_idx].ref
48
+ ):
49
+ del outs[-1]
50
+ outs.append(
51
+ PhonemeGroup(
52
+ ref=ph_groups[curr_idx].ref,
53
+ ref_idx=ph_groups[curr_idx].ref_idx,
54
+ out=ph_groups[prev_idx].out,
55
+ out_idx=ph_groups[prev_idx].out_idx,
56
+ )
57
+ )
58
+ # ref is part of out
59
+ elif (
60
+ ph_groups[prev_idx].ref_idx is not None
61
+ and ph_groups[curr_idx].out_idx is not None
62
+ and ph_groups[prev_idx].ref in ph_groups[curr_idx].out
63
+ ):
64
+ del outs[-1]
65
+ outs.append(
66
+ PhonemeGroup(
67
+ ref=ph_groups[prev_idx].ref,
68
+ ref_idx=ph_groups[prev_idx].ref_idx,
69
+ out=ph_groups[curr_idx].out,
70
+ out_idx=ph_groups[curr_idx].out_idx,
71
+ )
72
+ )
73
+ else:
74
+ outs.append(ph_groups[curr_idx])
75
+ prev_idx = curr_idx
76
+ return outs
77
+
78
+
79
+ def segment_groups(
80
+ ref_groups: list[str],
81
+ groups: list[str],
82
+ diffs,
83
+ ) -> list[PhonemeGroup]:
84
+ """Join similar phonmes groups and diffrentiate between groups"""
85
+ ref_counter = 0
86
+ ref_ptr = 0
87
+ ref_group_idx = 0
88
+ out_counter = 0
89
+ out_ptr = 0
90
+ out_group_idx = 0
91
+
92
+ out_pairs = []
93
+ for op, data in diffs:
94
+ if op == 0:
95
+ ref_counter += len(data)
96
+ out_counter += len(data)
97
+ elif op == 1:
98
+ out_counter += len(data)
99
+ elif op == -1:
100
+ ref_counter += len(data)
101
+
102
+ ref_has_match = True
103
+ out_has_match = True
104
+ while ref_has_match or out_has_match:
105
+ pair = PhonemeGroup()
106
+ if ref_group_idx < len(ref_groups):
107
+ if (ref_counter - ref_ptr) >= len(ref_groups[ref_group_idx]):
108
+ pair.ref = ref_groups[ref_group_idx]
109
+ pair.ref_idx = ref_group_idx
110
+ ref_ptr += len(ref_groups[ref_group_idx])
111
+ ref_group_idx += 1
112
+ else:
113
+ ref_has_match = False
114
+ else:
115
+ ref_has_match = False
116
+
117
+ if out_group_idx < len(groups):
118
+ if (out_counter - out_ptr) >= len(groups[out_group_idx]):
119
+ pair.out = groups[out_group_idx]
120
+ pair.out_idx = out_group_idx
121
+ out_ptr += len(groups[out_group_idx])
122
+ out_group_idx += 1
123
+ else:
124
+ out_has_match = False
125
+ else:
126
+ out_has_match = False
127
+
128
+ if pair.ref or pair.out:
129
+ out_pairs.append(pair)
130
+ return merge_same_phoneme_group(out_pairs)
131
+
132
+
133
+ def expalin_sifat(
134
+ sifat: list[Sifa],
135
+ exp_sifat: list[SifaOutput],
136
+ diffs,
137
+ ):
138
+ table = []
139
+ chunks = [s.phonemes_group for s in sifat]
140
+ exp_chunks = [s.phonemes for s in exp_sifat]
141
+
142
+ groups = segment_groups(ref_groups=exp_chunks, groups=chunks, diffs=diffs)
143
+ keys = set(asdict(sifat[0]).keys()) - {"phonemes_group"}
144
+ madd_group = alph.phonetics.alif + alph.phonetics.yaa_madd + alph.phonetics.waw_madd
145
+
146
+ for group in groups:
147
+ raw = {}
148
+ tag = group.get_tag()
149
+ if (tag == "exact") or (tag == "partial" and group.ref[0] in madd_group):
150
+ raw["tag"] = "exact"
151
+ raw["phonemes"] = sifat[group.out_idx].phonemes_group
152
+ raw["exp_phonemes"] = exp_sifat[group.ref_idx].phonemes
153
+ for key in keys:
154
+ if getattr(sifat[group.out_idx], key) is not None:
155
+ raw[f"{key}"] = getattr(sifat[group.out_idx], key).text
156
+ else:
157
+ raw[f"{key}"] = "None"
158
+
159
+ raw[f"exp_{key}"] = getattr(exp_sifat[group.ref_idx], key)
160
+ elif tag in {"partial", "insert"}:
161
+ raw["tag"] = "insert"
162
+ raw["phonemes"] = sifat[group.out_idx].phonemes_group
163
+ raw["exp_phonemes"] = ""
164
+ for key in keys:
165
+ if getattr(sifat[group.out_idx], key) is not None:
166
+ raw[f"{key}"] = getattr(sifat[group.out_idx], key).text
167
+ else:
168
+ raw[f"{key}"] = "None"
169
+
170
+ raw[f"exp_{key}"] = ""
171
+ if raw:
172
+ table.append(raw)
173
+
174
+ # print(json.dumps(table, indent=2, ensure_ascii=False))
175
+ return table
176
+
177
+
178
+ def print_sifat_table(
179
+ table: list[dict],
180
+ lang: Literal["arabic", "english"] = "arabic",
181
+ ):
182
+ """Print the sifat comparison table with rich highlighting"""
183
+ if not table:
184
+ return
185
+
186
+ # Create a rich Table
187
+ rich_table = Table()
188
+
189
+ # Get base columns (non-exp keys without 'tag')
190
+ base_keys = [k for k in table[0].keys() if not k.startswith("exp_") and k != "tag"]
191
+
192
+ # Add columns
193
+ # rich_table.add_column("Tag", style="cyan")
194
+ for key in base_keys:
195
+ rich_table.add_column(key.replace("_", " ").title())
196
+
197
+ # Add rows
198
+ for row in table:
199
+ tag = row["tag"]
200
+ values = []
201
+ for key in base_keys:
202
+ exp_key = f"exp_{key}"
203
+ value = str(row[key])
204
+ if key != "phonemes" and lang == "arabic":
205
+ value = SIFAT_ATTR_TO_ARABIC_WITHOUT_BRACKETS[value]
206
+
207
+ # Apply styling based on tag and comparison
208
+ if tag == "exact" and row.get(exp_key) != row[key]:
209
+ values.append(f"[red]{value}[/red]")
210
+ elif tag == "insert":
211
+ values.append(f"[yellow]{value}[/yellow]")
212
+ else:
213
+ values.append(value)
214
+
215
+ rich_table.add_row(*values)
216
+
217
+ # Print the table
218
+ console = Console()
219
+ console.print(rich_table)
220
+
221
+
222
+ def explain_for_terminal(
223
+ phonemes: str,
224
+ exp_phonemes: str,
225
+ sifat: list[Sifa],
226
+ exp_sifat: list[SifaOutput],
227
+ lang: Literal["arabic", "english"] = "english",
228
+ ):
229
+ # Create diff-match-patch object
230
+ dmp_obj = dmp.diff_match_patch()
231
+
232
+ # Calculate differences
233
+ diffs = dmp_obj.diff_main(exp_phonemes, phonemes)
234
+
235
+ # Create a Rich Text object for colored output
236
+ result = Text()
237
+
238
+ # Process each difference
239
+ for op, data in diffs:
240
+ if op == dmp_obj.DIFF_EQUAL:
241
+ result.append(data, style="white")
242
+ elif op == dmp_obj.DIFF_INSERT:
243
+ result.append(data, style="green")
244
+ elif op == dmp_obj.DIFF_DELETE:
245
+ result.append(data, style="red strike")
246
+
247
+ # Print the result
248
+ print(result)
249
+ sifat_table = expalin_sifat(sifat, exp_sifat, diffs)
250
+ print_sifat_table(sifat_table, lang=lang) # Add this line to print the table
src/quran_muaalem/explain_gradio.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Literal
2
+ import diff_match_patch as dmp
3
+
4
+ from .explain import expalin_sifat
5
+ from .modeling.vocab import SIFAT_ATTR_TO_ARABIC_WITHOUT_BRACKETS
6
+
7
+
8
+ def explain_for_gradio(
9
+ phonemes: str,
10
+ exp_phonemes: str,
11
+ sifat: list,
12
+ exp_sifat: list,
13
+ lang: Literal["arabic", "english"] = "english",
14
+ ) -> str:
15
+ # Create diff-match-patch object
16
+ dmp_obj = dmp.diff_match_patch()
17
+
18
+ # Calculate differences using Google's diff-match-patch (same as terminal)
19
+ diffs = dmp_obj.diff_main(exp_phonemes, phonemes)
20
+
21
+ # Create HTML for phoneme differences
22
+ phoneme_html = explain_phonemes_html(dmp_obj, diffs)
23
+
24
+ # Create HTML for sifat table using your existing function
25
+ sifat_table = expalin_sifat(sifat, exp_sifat, diffs)
26
+ sifat_html = explain_sifat_html(sifat_table, lang)
27
+
28
+ # Combine both sections
29
+ html_output = f"""
30
+ <div style="font-family: monospace; width: 100%;">
31
+ <h3>مقارنة الحروف</h3>
32
+ {phoneme_html}
33
+ <h3>مقارنة صفات الحروف</h3>
34
+ {sifat_html}
35
+ <div class="color-legend">
36
+ </div>
37
+ """
38
+
39
+ return html_output
40
+
41
+
42
+ def explain_phonemes_html(dmp_obj, diffs):
43
+ html_output = '<div style="background-color: #000; padding: 10px; border-radius: 5px; margin-bottom: 20px; font-size: 30px;">'
44
+
45
+ # Process each difference (same logic as terminal version)
46
+ for op, data in diffs:
47
+ if op == dmp_obj.DIFF_EQUAL:
48
+ html_output += f'<span style="color: #ffffff;">{data}</span>'
49
+ elif op == dmp_obj.DIFF_INSERT:
50
+ html_output += f'<span style="color: #00ff00;">{data}</span>'
51
+ elif op == dmp_obj.DIFF_DELETE:
52
+ html_output += f'<span style="color: #ff0000; text-decoration: line-through;">{data}</span>'
53
+
54
+ html_output += "</div>"
55
+ return html_output
56
+
57
+
58
+ def explain_sifat_html(table, lang):
59
+ if not table:
60
+ return "<p>No sifat data available</p>"
61
+
62
+ # Create HTML table with full width
63
+ html_output = """
64
+ <table style="width: 100%; border-collapse: collapse; background-color: #000; color: #fff; margin-bottom: 20px;">
65
+ <thead>
66
+ <tr>
67
+ """
68
+
69
+ # Get base columns (non-exp keys without 'tag')
70
+ base_keys = [k for k in table[0].keys() if not k.startswith("exp_") and k != "tag"]
71
+
72
+ # Add columns
73
+ for key in base_keys:
74
+ html_output += f'<th style="border: 1px solid #444; padding: 8px; text-align: left;">{key.replace("_", " ").title()}</th>'
75
+
76
+ html_output += """
77
+ </tr>
78
+ </thead>
79
+ <tbody>
80
+ """
81
+
82
+ # Add rows
83
+ for row in table:
84
+ tag = row["tag"]
85
+ html_output += "<tr>"
86
+
87
+ for key in base_keys:
88
+ exp_key = f"exp_{key}"
89
+ value = str(row[key])
90
+
91
+ # Apply Arabic translation if needed
92
+ if key != "phonemes" and lang == "arabic":
93
+ value = SIFAT_ATTR_TO_ARABIC_WITHOUT_BRACKETS.get(value, value)
94
+
95
+ # Apply styling based on tag and comparison
96
+ if tag == "exact" and row.get(exp_key) != row[key]:
97
+ html_output += f'<td style="border: 1px solid #444; padding: 8px; color: #ff0000;">{value}</td>'
98
+ elif tag == "insert":
99
+ html_output += f'<td style="border: 1px solid #444; padding: 8px; color: #ffff00;">{value}</td>'
100
+ else:
101
+ html_output += (
102
+ f'<td style="border: 1px solid #444; padding: 8px;">{value}</td>'
103
+ )
104
+
105
+ html_output += "</tr>"
106
+
107
+ html_output += """
108
+ </tbody>
109
+ </table>
110
+ """
111
+
112
+ return html_output
src/quran_muaalem/gradio_app.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from dataclasses import asdict
3
+ import json
4
+ from typing import Literal, Optional, Any, get_origin, get_args
5
+
6
+ from quran_transcript import Aya, quran_phonetizer, MoshafAttributes
7
+ from quran_transcript.utils import PartOfUthmaniWord
8
+ from quran_transcript.phonetics.moshaf_attributes import (
9
+ get_arabic_attributes,
10
+ get_arabic_name,
11
+ )
12
+ from librosa.core import load
13
+ from pydantic.fields import FieldInfo, PydanticUndefined
14
+ import torch
15
+ import gradio as gr
16
+
17
+ from quran_muaalem.inference import Muaalem
18
+ from quran_muaalem.muaalem_typing import MuaalemOutput
19
+ from quran_muaalem.explain import explain_for_terminal
20
+ from quran_muaalem.explain_gradio import explain_for_gradio
21
+
22
+ # Initialize components
23
+ REQUIRED_MOSHAF_FIELDS = [
24
+ "rewaya",
25
+ "takbeer",
26
+ "madd_monfasel_len",
27
+ "madd_mottasel_len",
28
+ "madd_mottasel_waqf",
29
+ "madd_aared_len",
30
+ "madd_alleen_len",
31
+ "ghonna_lam_and_raa",
32
+ "meem_aal_imran",
33
+ "madd_yaa_alayn_alharfy",
34
+ "saken_before_hamz",
35
+ "sakt_iwaja",
36
+ "sakt_marqdena",
37
+ "sakt_man_raq",
38
+ "sakt_bal_ran",
39
+ "sakt_maleeyah",
40
+ "between_anfal_and_tawba",
41
+ "noon_and_yaseen",
42
+ "yaa_ataan",
43
+ "start_with_ism",
44
+ "yabsut",
45
+ "bastah",
46
+ "almusaytirun",
47
+ "bimusaytir",
48
+ "tasheel_or_madd",
49
+ "yalhath_dhalik",
50
+ "irkab_maana",
51
+ "noon_tamnna",
52
+ "harakat_daaf",
53
+ "alif_salasila",
54
+ "idgham_nakhluqkum",
55
+ "raa_firq",
56
+ "raa_alqitr",
57
+ "raa_misr",
58
+ "raa_nudhur",
59
+ "raa_yasr",
60
+ "meem_mokhfah",
61
+ ]
62
+ model_id = "obadx/muaalem-model-v3_2"
63
+ logging.basicConfig(level=logging.INFO)
64
+ device = "cuda" if torch.cuda.is_available() else "cpu"
65
+ muaalem = Muaalem(model_name_or_path=model_id, device=device)
66
+ sampling_rate = 16000
67
+
68
+ # Load Sura information
69
+ sura_idx_to_name = {}
70
+ sura_to_aya_count = {}
71
+ start_aya = Aya()
72
+ for sura_idx in range(1, 115):
73
+ start_aya.set(sura_idx, 1)
74
+ sura_idx_to_name[sura_idx] = start_aya.get().sura_name
75
+ sura_to_aya_count[sura_idx] = start_aya.get().num_ayat_in_sura
76
+
77
+ # Default moshaf settings
78
+ default_moshaf = MoshafAttributes(
79
+ rewaya="hafs",
80
+ madd_monfasel_len=4,
81
+ madd_mottasel_len=4,
82
+ madd_mottasel_waqf=4,
83
+ madd_aared_len=4,
84
+ )
85
+
86
+ # Current moshaf settings (will be updated from settings page)
87
+ current_moshaf = default_moshaf
88
+
89
+
90
+ def get_field_name(field_name: str, field_info: FieldInfo) -> str:
91
+ """Return the Arabic name of the field if applicable else the field_name"""
92
+ label = field_name
93
+ arabic_name = get_arabic_name(field_info)
94
+ if arabic_name:
95
+ label = f"{arabic_name} ({field_name})"
96
+ return label
97
+
98
+
99
+ def create_gradio_input_for_field(
100
+ field_name: str,
101
+ field_info: FieldInfo,
102
+ default_value: Any = None,
103
+ key_prefix="model_",
104
+ help: str | None = None,
105
+ ) -> Any:
106
+ """Create a gradio input field given a pydantic field info"""
107
+ # Extract Arabic name from field description if available
108
+ label = get_field_name(field_name, field_info)
109
+
110
+ if default_value is None:
111
+ if field_info.default != PydanticUndefined:
112
+ default_value = field_info.default
113
+
114
+ if help is None:
115
+ help = field_info.description
116
+
117
+ # Handle Literal types
118
+ if get_origin(field_info.annotation) is Literal:
119
+ choices = list(get_args(field_info.annotation))
120
+ arabic_attributes = get_arabic_attributes(field_info)
121
+
122
+ # Create choice list with Arabic labels if available
123
+ choice_list = []
124
+ for choice in choices:
125
+ if arabic_attributes and choice in arabic_attributes:
126
+ choice_list.append((arabic_attributes[choice], choice))
127
+ else:
128
+ choice_list.append((str(choice), choice))
129
+
130
+ return gr.Dropdown(
131
+ choices=choice_list,
132
+ value=default_value,
133
+ label=label,
134
+ info=help,
135
+ interactive=True,
136
+ )
137
+
138
+ # Handle different field types
139
+ if field_info.annotation in [str, Optional[str]]:
140
+ return gr.Textbox(value=default_value or "", label=label, info=help)
141
+ elif field_info.annotation in [int, Optional[int]]:
142
+ return gr.Number(value=default_value or 0, label=label, info=help, precision=0)
143
+ elif field_info.annotation in [float, Optional[float]]:
144
+ return gr.Number(
145
+ value=default_value or 0.0, label=label, info=help, precision=1
146
+ )
147
+ elif field_info.annotation in [bool, Optional[bool]]:
148
+ return gr.Checkbox(value=default_value or False, label=label, info=help)
149
+
150
+ raise ValueError(f"Unsupported field type for {label}: {field_info.annotation}")
151
+
152
+
153
+ def update_aya_dropdown(sura_idx):
154
+ if not sura_idx:
155
+ sura_idx = 1
156
+ return gr.update(
157
+ choices=list(range(1, sura_to_aya_count[int(sura_idx)] + 1)), value=1
158
+ )
159
+
160
+
161
+ def update_uthmani_ref(sura_idx, aya_idx, start_idx, num_words):
162
+ if not all([sura_idx, aya_idx, start_idx is not None, num_words is not None]):
163
+ return ""
164
+ try:
165
+ uthmani_ref = (
166
+ Aya(int(sura_idx), int(aya_idx))
167
+ .get_by_imlaey_words(int(start_idx), int(num_words))
168
+ .uthmani
169
+ )
170
+ return uthmani_ref
171
+ except PartOfUthmaniWord as e:
172
+ return f"⚠️ Warning: You've selected part of a Uthmani word. Please adjust the number of words to include complete words only.\n\nError details: {str(e)}"
173
+ except Exception as e:
174
+ return f"Error: {str(e)}"
175
+
176
+
177
+ def process_audio(audio, sura_idx, aya_idx, start_idx, num_words):
178
+ global current_moshaf
179
+
180
+ if audio is None:
181
+ return "Please upload an audio file first"
182
+
183
+ try:
184
+ # Get Uthmani reference text
185
+ uthmani_ref = (
186
+ Aya(int(sura_idx), int(aya_idx))
187
+ .get_by_imlaey_words(int(start_idx), int(num_words))
188
+ .uthmani
189
+ )
190
+ phonetizer_out = quran_phonetizer(
191
+ uthmani_ref, current_moshaf, remove_spaces=True
192
+ )
193
+
194
+ # Process audio
195
+ wave, _ = load(audio, sr=sampling_rate, mono=True)
196
+ outs = muaalem(
197
+ [wave],
198
+ [phonetizer_out],
199
+ sampling_rate=sampling_rate,
200
+ )
201
+
202
+ # # Prepare output
203
+ # output_text = f"Phonemes: {outs[0].phonemes}\n\n"
204
+ # for sifa in outs[0].sifat:
205
+ # output_text += json.dumps(asdict(sifa), indent=2, ensure_ascii=False) + "\n"
206
+ # output_text += "*" * 30 + "\n"
207
+ # output_text += "-" * 40 + "\n\n"
208
+
209
+ # Add explanation
210
+ explanation_html = explain_for_gradio(
211
+ outs[0].phonemes.text,
212
+ phonetizer_out.phonemes,
213
+ outs[0].sifat,
214
+ phonetizer_out.sifat,
215
+ lang="arabic",
216
+ )
217
+
218
+ return explanation_html
219
+
220
+ except PartOfUthmaniWord as e:
221
+ return f"⚠️ Error: The selected word range includes partial Uthmani words. Please adjust the number of words to include complete words only.\n\nError details: {str(e)}"
222
+ # except Exception as e:
223
+ # return f"Error processing audio: {str(e)}"
224
+
225
+
226
+ def update_moshaf_settings(*args):
227
+ """Update the global moshaf settings with values from the settings page"""
228
+ global current_moshaf, field_names
229
+
230
+ try:
231
+ # Create a dictionary from the field names and values
232
+ settings_dict = dict(zip(field_names, args))
233
+
234
+ # Create a new MoshafAttributes object with the updated values
235
+ current_moshaf = MoshafAttributes(**settings_dict)
236
+ return "✅ تم حفظ الإعدادات بنجاح - Settings saved successfully!"
237
+ except Exception as e:
238
+ return f"❌ خطأ في حفظ الإعدادات - Error saving settings: {str(e)}"
239
+
240
+
241
+ def reset_settings():
242
+ """Reset all settings to default values"""
243
+ global current_moshaf
244
+
245
+ try:
246
+ current_moshaf = default_moshaf
247
+ # Return default values for all fields
248
+ default_values = [
249
+ getattr(default_moshaf, field_name) for field_name in field_names
250
+ ]
251
+ return default_values + [
252
+ "✅ تم إعادة التعيين إلى الإعدادات الافتراضية - Reset to default settings successfully!"
253
+ ]
254
+ except Exception as e:
255
+ return [getattr(current_moshaf, field_name) for field_name in field_names] + [
256
+ f"❌ Error resetting settings: {str(e)}"
257
+ ]
258
+
259
+
260
+ # Create the Gradio app
261
+ with gr.Blocks(title="المعلم القرآني") as app:
262
+ # Store current moshaf settings in session state
263
+ current_moshaf_state = gr.State(default_moshaf)
264
+
265
+ # Initialize field names list
266
+ field_names = []
267
+
268
+ with gr.Tab("التحليل الرئيسي - Main Analysis"):
269
+ gr.Markdown("# كشف أخطاء التلاوة والتجويد وصفات الحروف")
270
+ gr.Markdown("اختر المقطع القرآني المراد تعلمه")
271
+
272
+ with gr.Row():
273
+ with gr.Column(scale=1):
274
+ gr.Markdown("### التلاة المقارنة")
275
+
276
+ # Create sura dropdown with both index and name
277
+ sura_choices = [
278
+ (f"{idx} - {sura_idx_to_name[idx]}", idx) for idx in range(1, 115)
279
+ ]
280
+ sura_dropdown = gr.Dropdown(
281
+ choices=sura_choices,
282
+ label="السورة",
283
+ value=1,
284
+ elem_id="sura_dropdown",
285
+ )
286
+
287
+ aya_dropdown = gr.Dropdown(
288
+ choices=list(range(1, sura_to_aya_count[1] + 1)),
289
+ label="رقم الآية",
290
+ value=1,
291
+ elem_id="aya_dropdown",
292
+ )
293
+ start_idx = gr.Number(
294
+ value=0,
295
+ label="رقمة الكلمة بداية من صفر (Word Index)",
296
+ minimum=0,
297
+ step=1,
298
+ elem_id="start_idx",
299
+ )
300
+ num_words = gr.Number(
301
+ value=4,
302
+ label="عدد الكلمات",
303
+ minimum=1,
304
+ step=1,
305
+ elem_id="num_words",
306
+ )
307
+ uthmani_text = gr.Textbox(
308
+ label="الرسم العثماني",
309
+ interactive=False,
310
+ elem_id="uthmani_text",
311
+ )
312
+
313
+ with gr.Column(scale=2):
314
+ gr.Markdown("### فحص التلاوة القرآنية")
315
+ audio_input = gr.Audio(
316
+ sources=["upload", "microphone"],
317
+ label="Upload or Record Audio",
318
+ type="filepath",
319
+ elem_id="audio_input",
320
+ )
321
+ analyze_btn = gr.Button(
322
+ "افحص التلاوة", variant="primary", elem_id="analyze_btn"
323
+ )
324
+ output_html = gr.HTML(
325
+ label="نتيجة الفحص",
326
+ elem_id="output_html",
327
+ )
328
+
329
+ # Initial update of uthmani text
330
+ app.load(
331
+ update_uthmani_ref,
332
+ inputs=[sura_dropdown, aya_dropdown, start_idx, num_words],
333
+ outputs=uthmani_text,
334
+ )
335
+
336
+ # Update aya dropdown when sura changes and reset aya_idx to 1
337
+ sura_dropdown.change(
338
+ update_aya_dropdown, inputs=sura_dropdown, outputs=aya_dropdown
339
+ ).then(
340
+ update_uthmani_ref,
341
+ inputs=[sura_dropdown, aya_dropdown, start_idx, num_words],
342
+ outputs=uthmani_text,
343
+ )
344
+
345
+ # Update uthmani text when any parameter changes
346
+ for component in [aya_dropdown, start_idx, num_words]:
347
+ component.change(
348
+ update_uthmani_ref,
349
+ inputs=[sura_dropdown, aya_dropdown, start_idx, num_words],
350
+ outputs=uthmani_text,
351
+ )
352
+
353
+ # Process audio when button is clicked
354
+ analyze_btn.click(
355
+ process_audio,
356
+ inputs=[audio_input, sura_dropdown, aya_dropdown, start_idx, num_words],
357
+ outputs=output_html,
358
+ )
359
+
360
+ with gr.Tab("إعدادات المصحف - Moshaf Settings"):
361
+ gr.Markdown("# إعدادات خصائص المصحف")
362
+ gr.Markdown("قم بتعديل خصائص المصحف حسب التلاوة المطلوبة")
363
+
364
+ # Create settings inputs directly in the tab
365
+ settings_components = []
366
+ fields = MoshafAttributes.model_fields
367
+
368
+ # Create inputs for all required fields
369
+ for field_name in REQUIRED_MOSHAF_FIELDS:
370
+ field_info = fields[field_name]
371
+ input_component = create_gradio_input_for_field(
372
+ field_name, field_info, getattr(default_moshaf, field_name, None)
373
+ )
374
+ settings_components.append(input_component)
375
+ field_names.append(field_name)
376
+
377
+ # Save button and status message
378
+ with gr.Row():
379
+ save_btn = gr.Button("حفظ الإعدادات - Save Settings", variant="primary")
380
+ reset_btn = gr.Button("إعادة التعيين - Reset to Default")
381
+
382
+ status_message = gr.Markdown()
383
+
384
+ # Save settings event
385
+ save_btn.click(
386
+ update_moshaf_settings, inputs=settings_components, outputs=status_message
387
+ )
388
+
389
+ # Reset to default event
390
+ reset_btn.click(
391
+ reset_settings, inputs=[], outputs=settings_components + [status_message]
392
+ )
393
+
394
+
395
+ def main(app=app):
396
+ app.launch(server_name="0.0.0.0", share=True)
397
+
398
+
399
+ if __name__ == "__main__":
400
+ main()
401
+ # app.launch(server_name="0.0.0.0", share=True)
src/quran_muaalem/inference.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ from quran_transcript import chunck_phonemes, QuranPhoneticScriptOutput
4
+ from transformers import AutoFeatureExtractor
5
+ import torch
6
+ from numpy.typing import NDArray
7
+
8
+ from .modeling.multi_level_tokenizer import MultiLevelTokenizer
9
+ from .modeling.modeling_multi_level_ctc import Wav2Vec2BertForMultilevelCTC
10
+ from .decode import (
11
+ multilevel_greedy_decode,
12
+ phonemes_level_greedy_decode,
13
+ )
14
+ from .muaalem_typing import Unit, SingleUnit, Sifa, MuaalemOutput
15
+
16
+
17
+ def format_sifat(
18
+ level_to_units: dict[str, list[Unit]],
19
+ chunked_phonemes_batch: list[list[str]],
20
+ multi_level_tokenizer: MultiLevelTokenizer,
21
+ ) -> list[list[Sifa]]:
22
+ sifat_batch = []
23
+ for seq_idx in range(len(chunked_phonemes_batch)):
24
+ sifat = []
25
+ for idx, ph_group in enumerate(chunked_phonemes_batch[seq_idx]):
26
+ sifa_dict = {}
27
+ for level in level_to_units:
28
+ if level == "phonemes":
29
+ continue
30
+ sifa_idx = idx
31
+ if sifa_idx < len(level_to_units[level][seq_idx].ids):
32
+ label = int(level_to_units[level][seq_idx].ids[sifa_idx])
33
+ text = multi_level_tokenizer.sifat_to_en_vocab[level][label]
34
+ p = level_to_units[level][seq_idx].probs[sifa_idx]
35
+ sifa_dict[level] = SingleUnit(
36
+ text=text, prob=float(p), idx=int(label)
37
+ )
38
+ else:
39
+ logging.info(
40
+ f"Sequence: `{seq_idx}` has short Level: {level} we will place it with `None`"
41
+ )
42
+ sifa_dict[level] = None
43
+ sifat.append(
44
+ Sifa(
45
+ phonemes_group=chunked_phonemes_batch[seq_idx][idx],
46
+ **sifa_dict,
47
+ )
48
+ )
49
+ sifat_batch.append(sifat)
50
+ return sifat_batch
51
+
52
+
53
+ class Muaalem:
54
+ def __init__(
55
+ self,
56
+ model_name_or_path: str = "obadx/muaalem-model-v3_2",
57
+ device: str = "cpu",
58
+ dtype=torch.bfloat16,
59
+ ):
60
+ """
61
+ Initializing Muallem Model
62
+
63
+ Args:
64
+ model_name_or_path: the huggingface model name or path
65
+ device: the device to run model on
66
+ dtype: the torch dtype. Default is `torch.bfloat16` as the model was trained on
67
+ """
68
+ self.device = device
69
+ self.dtype = dtype
70
+
71
+ self.model = Wav2Vec2BertForMultilevelCTC.from_pretrained(model_name_or_path)
72
+ self.multi_level_tokenizer = MultiLevelTokenizer(model_name_or_path)
73
+ self.processor = AutoFeatureExtractor.from_pretrained(model_name_or_path)
74
+
75
+ self.model.to(device, dtype=dtype)
76
+
77
+ @torch.no_grad()
78
+ def __call__(
79
+ self,
80
+ waves: list[list[float] | torch.FloatTensor | NDArray],
81
+ ref_quran_phonetic_script_list: list[QuranPhoneticScriptOutput],
82
+ sampling_rate: int,
83
+ ) -> list[MuaalemOutput]:
84
+ """Infrence Funcion for the Quran Muaalem Project
85
+
86
+ waves: input waves batch , seq_len with different formats described above
87
+ ref_quran_phonetic_script_list (list[QuranPhoneticScriptOutput]): list of the
88
+ phonetized ouput of `quran_transcript.quran_phonetizer` with `remove_space=True`
89
+
90
+ sampleing_rate (int): has to be 16000
91
+
92
+ Returns:
93
+ list[MuaalemOutput]:
94
+ A list of output objects, each containing phoneme predictions and their
95
+ phonetic features (sifat) for a processed input.
96
+
97
+ Each MuaalemOutput contains:
98
+ phonemes (Unit):
99
+ A dataclass representing the predicted phoneme sequence with:
100
+ text (str): Concatenated string of all phonemes.
101
+ probs (Union[torch.FloatTensor, list[float]]):
102
+ Confidence probabilities for each predicted phoneme.
103
+ ids (Union[torch.LongTensor, list[int]]):
104
+ Token IDs corresponding to each phoneme.
105
+
106
+ sifat (list[Sifa]):
107
+ A list of phonetic feature dataclasses (one per phoneme) with the
108
+ following optional properties (each is a SingleUnit or None):
109
+ - phonemes_group (str): the phonemes associated with the `sifa`
110
+ - hams_or_jahr (SingleUnit): either `hams` or `jahr`
111
+ - shidda_or_rakhawa (SingleUnit): either `shadeed`, `between`, or `rikhw`
112
+ - tafkheem_or_taqeeq (SingleUnit): either `mofakham`, `moraqaq`, or `low_mofakham`
113
+ - itbaq (SingleUnit): either `monfateh`, or `motbaq`
114
+ - safeer (SingleUnit): either `safeer`, or `no_safeer`
115
+ - qalqla (SingleUnit): eithr `moqalqal`, or `not_moqalqal`
116
+ - tikraar (SingleUnit): either `mokarar` or `not_mokarar`
117
+ - tafashie (SingleUnit): either `motafashie`, or `not_motafashie`
118
+ - istitala (SingleUnit): either `mostateel`, or `not_mostateel`
119
+ - ghonna (SingleUnit): either `maghnoon`, or `not_maghnoon`
120
+
121
+ Each SingleUnit in Sifa properties contains:
122
+ text (str): The feature's categorical label (e.g., "hams", "shidda").
123
+ prob (float): Confidence probability for this feature.
124
+ idx (int): Identifier for the feature class.
125
+ """
126
+
127
+ if sampling_rate != 16000:
128
+ raise ValueError(f"`sampling_rate` has to be 16000 got: `{sampling_rate}`")
129
+
130
+ # TODO: check input waves
131
+
132
+ # Tokanizing Ref
133
+ level_to_ref_ids = self.multi_level_tokenizer.tokenize(
134
+ [r.phonemes for r in ref_quran_phonetic_script_list],
135
+ [r.sifat for r in ref_quran_phonetic_script_list],
136
+ to_dict=True,
137
+ return_tensors="pt",
138
+ padding="longest",
139
+ )["input_ids"]
140
+
141
+ features = self.processor(
142
+ waves, sampling_rate=sampling_rate, return_tensors="pt"
143
+ )
144
+ features = {k: v.to(self.device, dtype=self.dtype) for k, v in features.items()}
145
+ outs = self.model(**features, return_dict=False)[0]
146
+
147
+ probs = {}
148
+ for level in outs:
149
+ probs[level] = (
150
+ torch.nn.functional.softmax(outs[level], dim=-1).cpu().to(torch.float32)
151
+ )
152
+
153
+ # Decoding only Phonemes Level
154
+ phonemes_units = phonemes_level_greedy_decode(
155
+ probs["phonemes"], self.multi_level_tokenizer.id_to_vocab["phonemes"]
156
+ )
157
+
158
+ chunked_phonemes_batch: list[list[str]] = []
159
+ for phonemes_unit in phonemes_units:
160
+ chunked_phonemes_batch.append(chunck_phonemes(phonemes_unit.text))
161
+
162
+ level_to_units = multilevel_greedy_decode(
163
+ level_to_probs=probs,
164
+ level_to_id_to_vocab=self.multi_level_tokenizer.id_to_vocab,
165
+ level_to_ref_ids=level_to_ref_ids,
166
+ chunked_phonemes_batch=chunked_phonemes_batch,
167
+ ref_chuncked_phonemes_batch=[
168
+ [s.phonemes for s in r.sifat] for r in ref_quran_phonetic_script_list
169
+ ],
170
+ phonemes_units=phonemes_units,
171
+ )
172
+
173
+ sifat_batch: list[list[Sifa]] = format_sifat(
174
+ level_to_units,
175
+ chunked_phonemes_batch,
176
+ self.multi_level_tokenizer,
177
+ )
178
+
179
+ outs = []
180
+ # looping over the batch
181
+ for idx in range(len(level_to_units["phonemes"])):
182
+ outs.append(
183
+ MuaalemOutput(
184
+ phonemes=level_to_units["phonemes"][idx],
185
+ sifat=sifat_batch[idx],
186
+ )
187
+ )
188
+ return outs
src/quran_muaalem/modeling/__init__.py ADDED
File without changes
src/quran_muaalem/modeling/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (177 Bytes). View file
 
src/quran_muaalem/modeling/__pycache__/configuration_multi_level_ctc.cpython-312.pyc ADDED
Binary file (17.5 kB). View file
 
src/quran_muaalem/modeling/__pycache__/modeling_multi_level_ctc.cpython-312.pyc ADDED
Binary file (6.77 kB). View file
 
src/quran_muaalem/modeling/__pycache__/multi_level_tokenizer.cpython-312.pyc ADDED
Binary file (6.24 kB). View file
 
src/quran_muaalem/modeling/__pycache__/vocab.cpython-312.pyc ADDED
Binary file (3.26 kB). View file
 
src/quran_muaalem/modeling/configuration_multi_level_ctc.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class Wav2Vec2BertForMultilevelCTCConfig(PretrainedConfig):
5
+ r"""
6
+ This is the configuration class to store the configuration of a [`Wav2Vec2BertModel`]. It is used to
7
+ instantiate an Wav2Vec2Bert model according to the specified arguments, defining the model architecture.
8
+ Instantiating a configuration with the defaults will yield a similar configuration to that of the Wav2Vec2Bert
9
+ [facebook/wav2vec2-bert-rel-pos-large](https://huggingface.co/facebook/wav2vec2-bert-rel-pos-large)
10
+ architecture.
11
+
12
+ Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
13
+ documentation from [`PretrainedConfig`] for more information.
14
+
15
+
16
+ Args:
17
+ level_to_vocab_size (`dict[str, int]`, *optional*):
18
+ Every level has its own vocabulary: {'phonemes': 44, 'hams_or_jahr': 3, ....}
19
+ Vocabulary size of the Wav2Vec2Bert model. Defines the number of different tokens that can be
20
+ represented by the `inputs_ids` passed when calling [`Wav2Vec2BertModel`]. Vocabulary size of the
21
+ model. Defines the different tokens that can be represented by the *inputs_ids* passed to the forward
22
+ method of [`Wav2Vec2BertModel`].
23
+ level_to_loss_weigth (`dict[str, int]`, *optional*):
24
+ Every level has its own loss weigth such that the sum of all levels adds to 1:
25
+ If you supply only one level: the rest of the level will have loss weigth of (1-given_loss_weigth) / nmber of rest of levels
26
+
27
+ hidden_size (`int`, *optional*, defaults to 1024):
28
+ Dimensionality of the encoder layers and the pooler layer.
29
+ num_hidden_layers (`int`, *optional*, defaults to 24):
30
+ Number of hidden layers in the Transformer encoder.
31
+ num_attention_heads (`int`, *optional*, defaults to 16):
32
+ Number of attention heads for each attention layer in the Transformer encoder.
33
+ intermediate_size (`int`, *optional*, defaults to 4096):
34
+ Dimensionality of the "intermediate" (i.e., feed-forward) layer in the Transformer encoder.
35
+ feature_projection_input_dim (`int`, *optional*, defaults to 160):
36
+ Input dimension of this model, i.e the dimension after processing input audios with [`SeamlessM4TFeatureExtractor`] or [`Wav2Vec2BertProcessor`].
37
+ hidden_act (`str` or `function`, *optional*, defaults to `"swish"`):
38
+ The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,
39
+ `"relu"`, `"selu"`, `"swish"` and `"gelu_new"` are supported.
40
+ hidden_dropout (`float`, *optional*, defaults to 0.0):
41
+ The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.
42
+ activation_dropout (`float`, *optional*, defaults to 0.0):
43
+ The dropout ratio for activations inside the fully connected layer.
44
+ attention_dropout (`float`, *optional*, defaults to 0.0):
45
+ The dropout ratio for the attention probabilities.
46
+ feat_proj_dropout (`float`, *optional*, defaults to 0.0):
47
+ The dropout probability for the feature projection.
48
+ final_dropout (`float`, *optional*, defaults to 0.1):
49
+ The dropout probability for the final projection layer of [`Wav2Vec2BertForCTC`].
50
+ layerdrop (`float`, *optional*, defaults to 0.1):
51
+ The LayerDrop probability. See the [LayerDrop paper](see https://huggingface.co/papers/1909.11556) for more
52
+ details.
53
+ initializer_range (`float`, *optional*, defaults to 0.02):
54
+ The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
55
+ layer_norm_eps (`float`, *optional*, defaults to 1e-05):
56
+ The epsilon used by the layer normalization layers.
57
+ apply_spec_augment (`bool`, *optional*, defaults to `True`):
58
+ Whether to apply *SpecAugment* data augmentation to the outputs of the feature encoder. For reference see
59
+ [SpecAugment: A Simple Data Augmentation Method for Automatic Speech
60
+ Recognition](https://huggingface.co/papers/1904.08779).
61
+ mask_time_prob (`float`, *optional*, defaults to 0.05):
62
+ Percentage (between 0 and 1) of all feature vectors along the time axis which will be masked. The masking
63
+ procedure generates `mask_time_prob*len(time_axis)/mask_time_length ``independent masks over the axis. If
64
+ reasoning from the probability of each feature vector to be chosen as the start of the vector span to be
65
+ masked, *mask_time_prob* should be `prob_vector_start*mask_time_length`. Note that overlap may decrease the
66
+ actual percentage of masked vectors. This is only relevant if `apply_spec_augment is True`.
67
+ mask_time_length (`int`, *optional*, defaults to 10):
68
+ Length of vector span along the time axis.
69
+ mask_time_min_masks (`int`, *optional*, defaults to 2):
70
+ The minimum number of masks of length `mask_feature_length` generated along the time axis, each time step,
71
+ irrespectively of `mask_feature_prob`. Only relevant if `mask_time_prob*len(time_axis)/mask_time_length <
72
+ mask_time_min_masks`.
73
+ mask_feature_prob (`float`, *optional*, defaults to 0.0):
74
+ Percentage (between 0 and 1) of all feature vectors along the feature axis which will be masked. The
75
+ masking procedure generates `mask_feature_prob*len(feature_axis)/mask_time_length` independent masks over
76
+ the axis. If reasoning from the probability of each feature vector to be chosen as the start of the vector
77
+ span to be masked, *mask_feature_prob* should be `prob_vector_start*mask_feature_length`. Note that overlap
78
+ may decrease the actual percentage of masked vectors. This is only relevant if `apply_spec_augment is
79
+ True`.
80
+ mask_feature_length (`int`, *optional*, defaults to 10):
81
+ Length of vector span along the feature axis.
82
+ mask_feature_min_masks (`int`, *optional*, defaults to 0):
83
+ The minimum number of masks of length `mask_feature_length` generated along the feature axis, each time
84
+ step, irrespectively of `mask_feature_prob`. Only relevant if
85
+ `mask_feature_prob*len(feature_axis)/mask_feature_length < mask_feature_min_masks`.
86
+ ctc_loss_reduction (`str`, *optional*, defaults to `"sum"`):
87
+ Specifies the reduction to apply to the output of `torch.nn.CTCLoss`. Only relevant when training an
88
+ instance of [`Wav2Vec2BertForCTC`].
89
+ ctc_zero_infinity (`bool`, *optional*, defaults to `False`):
90
+ Whether to zero infinite losses and the associated gradients of `torch.nn.CTCLoss`. Infinite losses mainly
91
+ occur when the inputs are too short to be aligned to the targets. Only relevant when training an instance
92
+ of [`Wav2Vec2BertForCTC`].
93
+ use_weighted_layer_sum (`bool`, *optional*, defaults to `False`):
94
+ Whether to use a weighted average of layer outputs with learned weights. Only relevant when using an
95
+ instance of [`Wav2Vec2BertForSequenceClassification`].
96
+ classifier_proj_size (`int`, *optional*, defaults to 768):
97
+ Dimensionality of the projection before token mean-pooling for classification.
98
+ tdnn_dim (`tuple[int]` or `list[int]`, *optional*, defaults to `(512, 512, 512, 512, 1500)`):
99
+ A tuple of integers defining the number of output channels of each 1D convolutional layer in the *TDNN*
100
+ module of the *XVector* model. The length of *tdnn_dim* defines the number of *TDNN* layers.
101
+ tdnn_kernel (`tuple[int]` or `list[int]`, *optional*, defaults to `(5, 3, 3, 1, 1)`):
102
+ A tuple of integers defining the kernel size of each 1D convolutional layer in the *TDNN* module of the
103
+ *XVector* model. The length of *tdnn_kernel* has to match the length of *tdnn_dim*.
104
+ tdnn_dilation (`tuple[int]` or `list[int]`, *optional*, defaults to `(1, 2, 3, 1, 1)`):
105
+ A tuple of integers defining the dilation factor of each 1D convolutional layer in *TDNN* module of the
106
+ *XVector* model. The length of *tdnn_dilation* has to match the length of *tdnn_dim*.
107
+ xvector_output_dim (`int`, *optional*, defaults to 512):
108
+ Dimensionality of the *XVector* embedding vectors.
109
+ pad_token_id (`int`, *optional*, defaults to 0): The id of the _beginning-of-stream_ token.
110
+ bos_token_id (`int`, *optional*, defaults to 1): The id of the _padding_ token.
111
+ eos_token_id (`int`, *optional*, defaults to 2): The id of the _end-of-stream_ token.
112
+ add_adapter (`bool`, *optional*, defaults to `False`):
113
+ Whether a convolutional attention network should be stacked on top of the Wav2Vec2Bert Encoder. Can be very
114
+ useful for warm-starting Wav2Vec2Bert for SpeechEncoderDecoder models.
115
+ adapter_kernel_size (`int`, *optional*, defaults to 3):
116
+ Kernel size of the convolutional layers in the adapter network. Only relevant if `add_adapter is True`.
117
+ adapter_stride (`int`, *optional*, defaults to 2):
118
+ Stride of the convolutional layers in the adapter network. Only relevant if `add_adapter is True`.
119
+ num_adapter_layers (`int`, *optional*, defaults to 1):
120
+ Number of convolutional layers that should be used in the adapter network. Only relevant if `add_adapter is
121
+ True`.
122
+ adapter_act (`str` or `function`, *optional*, defaults to `"relu"`):
123
+ The non-linear activation function (function or string) in the adapter layers. If string, `"gelu"`,
124
+ `"relu"`, `"selu"`, `"swish"` and `"gelu_new"` are supported.
125
+ use_intermediate_ffn_before_adapter (`bool`, *optional*, defaults to `False`):
126
+ Whether an intermediate feed-forward block should be stacked on top of the Wav2Vec2Bert Encoder and before the adapter network.
127
+ Only relevant if `add_adapter is True`.
128
+ output_hidden_size (`int`, *optional*):
129
+ Dimensionality of the encoder output layer. If not defined, this defaults to *hidden-size*. Only relevant
130
+ if `add_adapter is True`.
131
+ position_embeddings_type (`str`, *optional*, defaults to `"relative_key"`):
132
+ Can be specified to :
133
+ - `rotary`, for rotary position embeddings.
134
+ - `relative`, for relative position embeddings.
135
+ - `relative_key`, for relative position embeddings as defined by Shaw in [Self-Attention
136
+ with Relative Position Representations (Shaw et al.)](https://huggingface.co/papers/1803.02155).
137
+ If left to `None`, no relative position embeddings is applied.
138
+ rotary_embedding_base (`int`, *optional*, defaults to 10000):
139
+ If `"rotary"` position embeddings are used, defines the size of the embedding base.
140
+ max_source_positions (`int`, *optional*, defaults to 5000):
141
+ if `"relative"` position embeddings are used, defines the maximum source input positions.
142
+ left_max_position_embeddings (`int`, *optional*, defaults to 64):
143
+ If `"relative_key"` (aka Shaw) position embeddings are used, defines the left clipping value for relative positions.
144
+ right_max_position_embeddings (`int`, *optional*, defaults to 8):
145
+ If `"relative_key"` (aka Shaw) position embeddings are used, defines the right clipping value for relative positions.
146
+ conv_depthwise_kernel_size (`int`, *optional*, defaults to 31):
147
+ Kernel size of convolutional depthwise 1D layer in Conformer blocks.
148
+ conformer_conv_dropout (`float`, *optional*, defaults to 0.1):
149
+ The dropout probability for all convolutional layers in Conformer blocks.
150
+ Example:
151
+
152
+ ```python
153
+ >>> from transformers import Wav2Vec2BertConfig, Wav2Vec2BertModel
154
+
155
+ >>> # Initializing a Wav2Vec2Bert facebook/wav2vec2-bert-rel-pos-large style configuration
156
+ >>> configuration = Wav2Vec2BertConfig()
157
+
158
+ >>> # Initializing a model (with random weights) from the facebook/wav2vec2-bert-rel-pos-large style configuration
159
+ >>> model = Wav2Vec2BertModel(configuration)
160
+
161
+ >>> # Accessing the model configuration
162
+ >>> configuration = model.config
163
+ ```"""
164
+
165
+ model_type = "multi_level_ctc"
166
+
167
+ def __init__(
168
+ self,
169
+ level_to_vocab_size: dict[str, int] = {},
170
+ level_to_loss_weight: dict[str, float] = {"phonemes": 0.4},
171
+ hidden_size=1024,
172
+ num_hidden_layers=24,
173
+ num_attention_heads=16,
174
+ intermediate_size=4096,
175
+ feature_projection_input_dim=160,
176
+ hidden_act="swish",
177
+ hidden_dropout=0.0,
178
+ activation_dropout=0.0,
179
+ attention_dropout=0.0,
180
+ feat_proj_dropout=0.0,
181
+ final_dropout=0.1,
182
+ layerdrop=0.1,
183
+ initializer_range=0.02,
184
+ layer_norm_eps=1e-5,
185
+ apply_spec_augment=True,
186
+ mask_time_prob=0.05,
187
+ mask_time_length=10,
188
+ mask_time_min_masks=2,
189
+ mask_feature_prob=0.0,
190
+ mask_feature_length=10,
191
+ mask_feature_min_masks=0,
192
+ ctc_loss_reduction="sum",
193
+ ctc_zero_infinity=False,
194
+ use_weighted_layer_sum=False,
195
+ classifier_proj_size=768,
196
+ tdnn_dim=(512, 512, 512, 512, 1500),
197
+ tdnn_kernel=(5, 3, 3, 1, 1),
198
+ tdnn_dilation=(1, 2, 3, 1, 1),
199
+ xvector_output_dim=512,
200
+ pad_token_id=0,
201
+ bos_token_id=1,
202
+ eos_token_id=2,
203
+ add_adapter=False,
204
+ adapter_kernel_size=3,
205
+ adapter_stride=2,
206
+ num_adapter_layers=1,
207
+ adapter_act="relu",
208
+ use_intermediate_ffn_before_adapter=False,
209
+ output_hidden_size=None,
210
+ position_embeddings_type="relative_key",
211
+ rotary_embedding_base=10000,
212
+ max_source_positions=5000,
213
+ left_max_position_embeddings=64,
214
+ right_max_position_embeddings=8,
215
+ conv_depthwise_kernel_size=31,
216
+ conformer_conv_dropout=0.1,
217
+ **kwargs,
218
+ ):
219
+ super().__init__(
220
+ **kwargs,
221
+ pad_token_id=pad_token_id,
222
+ bos_token_id=bos_token_id,
223
+ eos_token_id=eos_token_id,
224
+ )
225
+ self.hidden_size = hidden_size
226
+ self.num_hidden_layers = num_hidden_layers
227
+ self.intermediate_size = intermediate_size
228
+ self.hidden_act = hidden_act
229
+ self.num_attention_heads = num_attention_heads
230
+ self.feature_projection_input_dim = feature_projection_input_dim
231
+ self.hidden_dropout = hidden_dropout
232
+ self.attention_dropout = attention_dropout
233
+ self.activation_dropout = activation_dropout
234
+ self.feat_proj_dropout = feat_proj_dropout
235
+ self.final_dropout = final_dropout
236
+ self.layerdrop = layerdrop
237
+ self.layer_norm_eps = layer_norm_eps
238
+ self.initializer_range = initializer_range
239
+ self.level_to_vocab_size = level_to_vocab_size
240
+ self.use_weighted_layer_sum = use_weighted_layer_sum
241
+ self.max_source_positions = max_source_positions
242
+
243
+ loss_weights_sum = sum(level_to_loss_weight.values())
244
+ if loss_weights_sum > 1:
245
+ raise ValueError(
246
+ f"The sum of loss weight per level has to be less than one! got: `{level_to_loss_weight}`"
247
+ )
248
+ unmentioned_loss_levels_count = len(
249
+ [l for l in self.level_to_vocab_size if l not in level_to_loss_weight]
250
+ )
251
+ for level in self.level_to_vocab_size:
252
+ if level not in level_to_loss_weight:
253
+ level_to_loss_weight[level] = (
254
+ 1 - loss_weights_sum
255
+ ) / unmentioned_loss_levels_count
256
+ self.level_to_loss_weight = level_to_loss_weight
257
+
258
+ if position_embeddings_type is not None and position_embeddings_type not in [
259
+ "rotary",
260
+ "relative",
261
+ "relative_key",
262
+ ]:
263
+ raise ValueError(
264
+ """
265
+ `position_embeddings_type` is not valid. It must be one of the following values:
266
+ `["rotary", "relative", "relative_key"]` or left as `None`.
267
+ """
268
+ )
269
+ self.position_embeddings_type = position_embeddings_type
270
+ self.rotary_embedding_base = rotary_embedding_base
271
+ self.left_max_position_embeddings = left_max_position_embeddings
272
+ self.right_max_position_embeddings = right_max_position_embeddings
273
+
274
+ # Conformer-block related
275
+ self.conv_depthwise_kernel_size = conv_depthwise_kernel_size
276
+ self.conformer_conv_dropout = conformer_conv_dropout
277
+
278
+ # fine-tuning config parameters for SpecAugment: https://huggingface.co/papers/1904.08779
279
+ self.apply_spec_augment = apply_spec_augment
280
+ self.mask_time_prob = mask_time_prob
281
+ self.mask_time_length = mask_time_length
282
+ self.mask_time_min_masks = mask_time_min_masks
283
+ self.mask_feature_prob = mask_feature_prob
284
+ self.mask_feature_length = mask_feature_length
285
+ self.mask_feature_min_masks = mask_feature_min_masks
286
+
287
+ # ctc loss
288
+ self.ctc_loss_reduction = ctc_loss_reduction
289
+ self.ctc_zero_infinity = ctc_zero_infinity
290
+
291
+ # adapter
292
+ self.add_adapter = add_adapter
293
+ self.adapter_kernel_size = adapter_kernel_size
294
+ self.adapter_stride = adapter_stride
295
+ self.num_adapter_layers = num_adapter_layers
296
+ self.adapter_act = adapter_act
297
+ self.output_hidden_size = (
298
+ output_hidden_size if output_hidden_size is not None else hidden_size
299
+ )
300
+ if use_intermediate_ffn_before_adapter and not add_adapter:
301
+ raise ValueError(
302
+ "`use_intermediate_ffn_before_adapter` is `True` but `add_adapter` is `False`."
303
+ )
304
+ self.use_intermediate_ffn_before_adapter = use_intermediate_ffn_before_adapter
305
+
306
+ # SequenceClassification-specific parameter. Feel free to ignore for other classes.
307
+ self.classifier_proj_size = classifier_proj_size
308
+
309
+ # XVector-specific parameters. Feel free to ignore for other classes.
310
+ self.tdnn_dim = list(tdnn_dim)
311
+ self.tdnn_kernel = list(tdnn_kernel)
312
+ self.tdnn_dilation = list(tdnn_dilation)
313
+ self.xvector_output_dim = xvector_output_dim
314
+
315
+ @property
316
+ def inputs_to_logits_ratio(self):
317
+ ratio = self.feature_projection_input_dim * 2
318
+ if self.add_adapter:
319
+ ratio = ratio * (self.adapter_stride**self.num_adapter_layers)
320
+ return ratio
src/quran_muaalem/modeling/modeling_multi_level_ctc.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional, Union
2
+
3
+ from transformers.models.wav2vec2_bert.modeling_wav2vec2_bert import (
4
+ Wav2Vec2BertPreTrainedModel,
5
+ Wav2Vec2BertModel,
6
+ _HIDDEN_STATES_START_POSITION,
7
+ )
8
+ from transformers.utils import auto_docstring
9
+ from transformers.modeling_outputs import CausalLMOutput
10
+ import torch
11
+ from torch import nn
12
+
13
+ from .configuration_multi_level_ctc import Wav2Vec2BertForMultilevelCTCConfig
14
+
15
+
16
+ class Wav2Vec2BertForMultilevelCTC(Wav2Vec2BertPreTrainedModel):
17
+ config_class = Wav2Vec2BertForMultilevelCTCConfig
18
+
19
+ def __init__(self, config):
20
+ super().__init__(config)
21
+
22
+ self.wav2vec2_bert = Wav2Vec2BertModel(config)
23
+ self.dropout = nn.Dropout(config.final_dropout)
24
+
25
+ if config.level_to_vocab_size == {}:
26
+ raise ValueError(
27
+ f"You are trying to instantiate {self.__class__} with a configuration that "
28
+ "does not define the vocabulary size of the language model head. Please "
29
+ "instantiate the model as follows: `Wav2Vec2BertForCTC.from_pretrained(..., level_to_vocab_size=level_to_vocab_size)`. "
30
+ "or define `level_to_vocab_size` of your model's configuration."
31
+ )
32
+ output_hidden_size = (
33
+ config.output_hidden_size
34
+ if hasattr(config, "add_adapter") and config.add_adapter
35
+ else config.hidden_size
36
+ )
37
+ self.level_to_lm_head = nn.ModuleDict(
38
+ {
39
+ level: nn.Linear(output_hidden_size, vocab_size)
40
+ for level, vocab_size in config.level_to_vocab_size.items()
41
+ }
42
+ )
43
+
44
+ # Initialize weights and apply final processing
45
+ self.post_init()
46
+
47
+ @auto_docstring
48
+ def forward(
49
+ self,
50
+ input_features: Optional[torch.Tensor],
51
+ attention_mask: Optional[torch.Tensor] = None,
52
+ output_attentions: Optional[bool] = None,
53
+ output_hidden_states: Optional[bool] = None,
54
+ return_dict: Optional[bool] = None,
55
+ labels: Optional[dict[str, torch.Tensor]] = None,
56
+ ) -> Union[tuple, CausalLMOutput]:
57
+ r"""
58
+ labels (dict[`str`, `torch.LongTensor`] level_name to its labels of shape `(batch_size, target_length)`, *optional*):
59
+ Labels for connectionist temporal classification. Note that `target_length` has to be smaller or equal to
60
+ the sequence length of the output logits. Indices are selected in `[-100, 0, ..., config.vocab_size - 1]`.
61
+ All labels set to `-100` are ignored (masked), the loss is only computed for labels in `[0, ...,
62
+ config.vocab_size - 1]`.
63
+ """
64
+ if labels is not None:
65
+ if not isinstance(labels, dict):
66
+ raise ValueError(
67
+ f"Label has to be a dict for level to its tartget labels got `{type(labels)}`"
68
+ )
69
+ for level in labels:
70
+ if labels[level].max() >= self.config.level_to_vocab_size[level]:
71
+ raise ValueError(
72
+ f"Label values must be <= vocab_size: {self.config.level_to_vocab_size[level]} for level: `{level}`"
73
+ )
74
+
75
+ return_dict = (
76
+ return_dict if return_dict is not None else self.config.use_return_dict
77
+ )
78
+
79
+ outputs = self.wav2vec2_bert(
80
+ input_features,
81
+ attention_mask=attention_mask,
82
+ output_attentions=output_attentions,
83
+ output_hidden_states=output_hidden_states,
84
+ return_dict=return_dict,
85
+ )
86
+
87
+ hidden_states = outputs[0]
88
+ hidden_states = self.dropout(hidden_states)
89
+
90
+ level_to_logits = {}
91
+ for level in self.level_to_lm_head:
92
+ level_to_logits[level] = self.level_to_lm_head[level](hidden_states)
93
+
94
+ loss = None
95
+ if labels is not None:
96
+ # retrieve loss input_lengths from attention_mask
97
+ attention_mask = (
98
+ attention_mask
99
+ if attention_mask is not None
100
+ else torch.ones(
101
+ input_features.shape[:2],
102
+ device=input_features.device,
103
+ dtype=torch.long,
104
+ )
105
+ )
106
+ input_lengths = self._get_feat_extract_output_lengths(
107
+ attention_mask.sum([-1])
108
+ ).to(torch.long)
109
+
110
+ loss = 0.0
111
+ for level in labels:
112
+ # assuming that padded tokens are filled with -100
113
+ # when not being attended to
114
+ labels_mask = labels[level] >= 0
115
+ target_lengths = labels_mask.sum(-1)
116
+ flattened_targets = labels[level].masked_select(labels_mask)
117
+
118
+ # ctc_loss doesn't support fp16
119
+ log_probs = nn.functional.log_softmax(
120
+ level_to_logits[level], dim=-1, dtype=torch.float32
121
+ ).transpose(0, 1)
122
+
123
+ with torch.backends.cudnn.flags(enabled=False):
124
+ loss += self.config.level_to_loss_weight[
125
+ level
126
+ ] * nn.functional.ctc_loss(
127
+ log_probs,
128
+ flattened_targets,
129
+ input_lengths,
130
+ target_lengths,
131
+ blank=self.config.pad_token_id,
132
+ reduction=self.config.ctc_loss_reduction,
133
+ zero_infinity=self.config.ctc_zero_infinity,
134
+ )
135
+
136
+ if not return_dict:
137
+ output = (level_to_logits,) + outputs[_HIDDEN_STATES_START_POSITION:]
138
+ return ((loss,) + output) if loss is not None else output
139
+
140
+ return CausalLMOutput(
141
+ loss=loss,
142
+ logits=level_to_logits,
143
+ hidden_states=outputs.hidden_states,
144
+ attentions=outputs.attentions,
145
+ )
146
+
147
+
148
+ __all__ = ["Wav2Vec2BertForMultilevelCTC"]
src/quran_muaalem/modeling/multi_level_tokenizer.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import get_origin, Literal, Any
2
+
3
+ from quran_transcript import SifaOutput, quran_phonetizer
4
+ from transformers import Wav2Vec2CTCTokenizer
5
+
6
+ from .vocab import PAD_TOKEN, PAD_TOKEN_IDX, SIFAT_ATTR_TO_ARABIC, SIFAT_ATTR_TO_ENGLISH
7
+
8
+
9
+ def add_zero_between(L, x=PAD_TOKEN_IDX):
10
+ out = []
11
+ for i, item in enumerate(L):
12
+ out.append(item)
13
+ if i < len(L) - 1: # Don't add zero after the last element
14
+ out.append(0)
15
+ return out
16
+
17
+
18
+ class MultiLevelTokenizer:
19
+ def __init__(self, model_name_or_path: str):
20
+ self.levels = ["phonemes"]
21
+ for fieldname, fieldinfo in SifaOutput.model_fields.items():
22
+ if get_origin(fieldinfo.annotation) == Literal:
23
+ self.levels.append(fieldname)
24
+
25
+ self.level_to_tokenizer = {}
26
+ for level in self.levels:
27
+ self.level_to_tokenizer[level] = Wav2Vec2CTCTokenizer.from_pretrained(
28
+ model_name_or_path, pad_token=PAD_TOKEN, target_lang=level
29
+ )
30
+
31
+ self.level_to_id_vocab = self.get_level_to_id_to_voab()
32
+ self.sifat_level_to_id_to_en_vocab = self.get_sifat_levels_to_en_name()
33
+
34
+ def get_tokenizer(self):
35
+ return self.level_to_tokenizer["phonemes"]
36
+
37
+ @property
38
+ def vocab(self):
39
+ return self.get_tokenizer().vocab
40
+
41
+ @property
42
+ def id_to_vocab(self):
43
+ return self.level_to_id_vocab
44
+
45
+ @property
46
+ def sifat_to_en_vocab(self):
47
+ return self.sifat_level_to_id_to_en_vocab
48
+
49
+ def tokenize(
50
+ self,
51
+ phonetic_script: list[str] | str,
52
+ sifat: list[list[SifaOutput | dict]] | list[SifaOutput | dict],
53
+ to_dict=False,
54
+ **kwargs,
55
+ ) -> dict:
56
+ if isinstance(phonetic_script, str):
57
+ phonetic_script = [phonetic_script]
58
+ if not isinstance(sifat[0], list):
59
+ sifat = [sifat]
60
+
61
+ if isinstance(sifat[0][0], dict):
62
+ sifat = [[SifaOutput(**s) for s in inner_list] for inner_list in sifat]
63
+
64
+ level_to_text_list = {}
65
+ for level in self.levels:
66
+ if level == "phonemes":
67
+ text_list = phonetic_script
68
+ else:
69
+ text_list = [
70
+ "".join(
71
+ [SIFAT_ATTR_TO_ARABIC[getattr(s, level)] for s in inner_list]
72
+ )
73
+ for inner_list in sifat
74
+ ]
75
+ level_to_text_list[level] = text_list
76
+
77
+ level_to_tokenized = {}
78
+ for level in self.levels:
79
+ level_to_tokenized[level] = self.level_to_tokenizer[level](
80
+ level_to_text_list[level], **kwargs
81
+ )
82
+
83
+ if to_dict:
84
+ out_dict = {"input_ids": {}, "attention_mask": {}}
85
+ for level in level_to_tokenized:
86
+ for k in out_dict:
87
+ out_dict[k][level] = level_to_tokenized[level][k]
88
+ return out_dict
89
+ return level_to_tokenized
90
+
91
+ def decode(
92
+ self, level_to_input_ids: dict[str, Any], place_zeros_in_between=False
93
+ ) -> dict[str, list[str] | str]:
94
+ level_to_decoded_outs = {}
95
+ for level in level_to_input_ids:
96
+ input_ids = level_to_input_ids[level]
97
+ if place_zeros_in_between:
98
+ input_ids = [add_zero_between(ids) for ids in input_ids]
99
+ level_to_decoded_outs[level] = self.level_to_tokenizer[level].batch_decode(
100
+ input_ids,
101
+ )
102
+ return level_to_decoded_outs
103
+
104
+ def get_level_to_id_to_voab(self):
105
+ vocab = self.get_tokenizer().vocab
106
+ level_to_ids_to_vocab = {}
107
+ for level in vocab:
108
+ level_to_ids_to_vocab[level] = {v: k for k, v in vocab[level].items()}
109
+ return level_to_ids_to_vocab
110
+
111
+ def get_sifat_levels_to_en_name(self):
112
+ level_to_id_to_vocab = self.get_level_to_id_to_voab()
113
+ level_to_id_to_en_vocab = {}
114
+ for level in level_to_id_to_vocab:
115
+ if level == "phonemes":
116
+ continue
117
+ level_to_id_to_en_vocab[level] = {
118
+ k: SIFAT_ATTR_TO_ENGLISH[v] if k != PAD_TOKEN_IDX else PAD_TOKEN
119
+ for k, v in level_to_id_to_vocab[level].items()
120
+ }
121
+ return level_to_id_to_en_vocab
src/quran_muaalem/modeling/vocab.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from dataclasses import asdict
3
+ from typing import get_origin, get_args, Literal
4
+
5
+ from quran_transcript import alphabet as alph
6
+ from quran_transcript import SifaOutput
7
+
8
+ SIFAT_ATTR_TO_ARABIC_WITHOUT_BRACKETS = {
9
+ "hams": "همس",
10
+ "jahr": "جهر",
11
+ "shadeed": "شديد",
12
+ "between": "بين الشدة والرخاوة",
13
+ "rikhw": "رخو",
14
+ "mofakham": "مفخم",
15
+ "moraqaq": "مرقق",
16
+ "low_mofakham": "أدنى المفخم",
17
+ "monfateh": "منفتح",
18
+ "motbaq": "مطبق",
19
+ "safeer": "صفير",
20
+ "no_safeer": "لا صفير",
21
+ "moqalqal": "مقلقل",
22
+ "not_moqalqal": "لا قلقلة",
23
+ "mokarar": "مكرر",
24
+ "not_mokarar": "لا تكرار",
25
+ "motafashie": "متفشي",
26
+ "not_motafashie": "لا تفشي",
27
+ "mostateel": "مستطيل",
28
+ "not_mostateel": "لا إستطالة",
29
+ "maghnoon": "مغن",
30
+ "not_maghnoon": "لا غنة",
31
+ }
32
+ SIFAT_ATTR_TO_ARABIC = {
33
+ k: f"[{v}]" for k, v in SIFAT_ATTR_TO_ARABIC_WITHOUT_BRACKETS.items()
34
+ }
35
+ SIFAT_ATTR_TO_ENGLISH = {v: k for k, v in SIFAT_ATTR_TO_ARABIC.items()}
36
+
37
+ PAD_TOKEN = "[PAD]"
38
+ PAD_TOKEN_IDX = 0
39
+
40
+
41
+ def build_quran_phoneme_script_vocab(path: str):
42
+ level_to_token_to_idx = {}
43
+ # Phonemes level
44
+ phonemes = list(asdict(alph.phonetics).values())
45
+ level_to_token_to_idx["phonemes"] = {PAD_TOKEN: PAD_TOKEN_IDX}
46
+ idx = 0
47
+ for p in phonemes:
48
+ if idx == PAD_TOKEN_IDX:
49
+ idx += 1
50
+ level_to_token_to_idx["phonemes"][p] = idx
51
+ idx += 1
52
+
53
+ for field_name, fieldinfo in SifaOutput.model_fields.items():
54
+ if get_origin(fieldinfo.annotation) == Literal:
55
+ level = field_name
56
+ phonemes = get_args(fieldinfo.annotation)
57
+ phonemes = [SIFAT_ATTR_TO_ARABIC[p] for p in phonemes]
58
+ level_to_token_to_idx[level] = {PAD_TOKEN: PAD_TOKEN_IDX}
59
+ idx = 0
60
+ for p in phonemes:
61
+ if idx == PAD_TOKEN_IDX:
62
+ idx += 1
63
+ level_to_token_to_idx[level][p] = idx
64
+ idx += 1
65
+
66
+ with open(path, "w+", encoding="utf-8") as f:
67
+ json.dump(level_to_token_to_idx, f, ensure_ascii=False, indent=2)
src/quran_muaalem/muaalem_typing.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ import torch
3
+
4
+
5
+ @dataclass
6
+ class Unit:
7
+ """
8
+ probs: 1D tensors
9
+ """
10
+
11
+ text: str
12
+ probs: torch.FloatTensor | list[float]
13
+ ids: torch.LongTensor | list[int]
14
+
15
+
16
+ @dataclass
17
+ class SingleUnit:
18
+ """
19
+ A dataclass representing the predicted phoneme sequence with:
20
+ text (str): Concatenated string of all phonemes.
21
+ probs (Union[torch.FloatTensor, list[float]]):
22
+ Confidence probabilities for each predicted phoneme (1D tensor).
23
+ ids (Union[torch.LongTensor, list[int]]) (1D tensor):
24
+ Token IDs corresponding to each phoneme.
25
+
26
+ """
27
+
28
+ text: str
29
+ prob: float
30
+ idx: int
31
+
32
+
33
+ @dataclass
34
+ class Sifa:
35
+ """
36
+ following optional properties (each is a SingleUnit or None):
37
+ - phonemes_group (str): the phonemes associated with the `sifa`
38
+ - hams_or_jahr (SingleUnit): either `hams` or `jahr`
39
+ - shidda_or_rakhawa (SingleUnit): either `shadeed`, `between`, or `rikhw`
40
+ - tafkheem_or_taqeeq (SingleUnit): either `mofakham`, `moraqaq`, or `low_mofakham`
41
+ - itbaq (SingleUnit): either `monfateh`, or `motbaq`
42
+ - safeer (SingleUnit): either `safeer`, or `no_safeer`
43
+ - qalqla (SingleUnit): eithr `moqalqal`, or `not_moqalqal`
44
+ - tikraar (SingleUnit): either `mokarar` or `not_mokarar`
45
+ - tafashie (SingleUnit): either `motafashie`, or `not_motafashie`
46
+ - istitala (SingleUnit): either `mostateel`, or `not_mostateel`
47
+ - ghonna (SingleUnit): either `maghnoon`, or `not_maghnoon`
48
+
49
+ Each SingleUnit in Sifa properties contains:
50
+ text (str): The feature's categorical label (e.g., "hams", "shidda").
51
+ prob (float): Confidence probability for this feature.
52
+ idx (int): Identifier for the feature class.
53
+
54
+ """
55
+
56
+ phonemes_group: str
57
+ hams_or_jahr: SingleUnit | None
58
+ shidda_or_rakhawa: SingleUnit | None
59
+ tafkheem_or_taqeeq: SingleUnit | None
60
+ itbaq: SingleUnit | None
61
+ safeer: SingleUnit | None
62
+ qalqla: SingleUnit | None
63
+ tikraar: SingleUnit | None
64
+ tafashie: SingleUnit | None
65
+ istitala: SingleUnit | None
66
+ ghonna: SingleUnit | None
67
+
68
+
69
+ @dataclass
70
+ class MuaalemOutput:
71
+ """
72
+ text (str): The feature's categorical label (e.g., "hams", "shidda").
73
+ prob (float): Confidence probability for this feature.
74
+ idx (int): Identifier for the feature class.
75
+ """
76
+
77
+ phonemes: Unit
78
+ sifat: list[Sifa]