Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@
[colab-url]: https://colab.research.google.com/drive/1d9-mVu2eiPOPS9z5sS2V4TQ579xIUBi-?usp=sharing

# `quran-transcript` package
## 🆕 ما الجديد في الإصدار 0.5.1 (What's New in Version 0.5.1)

### 🎯 تحليل أخطاء التلاوة (Recitation Error Analysis)
- إضافة دالة `explain_error` لمقارنة النص الصوتي المتوقع (المرجع) مع النص الصوتي المُتنبأ به (مثلاً من قارئ أو نموذج تعلم آلي).
- توفير تحليل تفصيلي للأخطاء يشمل:
- نوع الخطأ: تجويدي (`tajweed`)، عادي (`normal`)، أو حركات (`tashkeel`).
- نوع الخطأ الكلامي: إدراج (`insert`)، حذف (`delete`)، أو استبدال (`replace`).
- قواعد التجويد المرتبطة بالخطأ (مثل المد، القلقلة، الغنة) مع تحديد الطول المتوقع والفعلي عند الاقتضاء.
- تمثيل النتائج باستخدام كائن `ReciterError` الذي يحتوي على معلومات دقيقة عن موقع الخطأ في النص العثماني والصوتي.
- هذه الأداة مفيدة لتقييم أداء قراء القرآن، وتحليل أخطاء نماذج التعرف على الكلام، وتقديم تغذية راجعة للمتعلمين.








## 🆕 ما الجديد في الإصدار 0.4.0 (What's New in Version 0.4.0)
Expand Down Expand Up @@ -253,6 +269,112 @@ print(f"النص العثماني: {uthmani_text}")
- `start`: موقع بداية المطابقة
- `end`: موقع نهاية المطابقة (غير شامل)

### 📖 مثال على تحليل أخطاء التلاوة (Error Analysis Example)

```python
from quran_transcript import (
quran_phonetizer,
MoshafAttributes,
ReciterError,
explain_error,
)

# إعداد خصائص المصحف
moshaf = MoshafAttributes(
rewaya="hafs",
madd_monfasel_len=4,
madd_mottasel_len=4,
madd_mottasel_waqf=4,
madd_aared_len=4,
)

# النص العثماني الأصلي
uthmani_text = "قَالُوٓا۟"

# نص صوتي متوقع (مرجعي)
ref_out = quran_phonetizer(uthmani_text, moshaf)
print("المرجع:", ref_out.phonemes)

# نص صوتي مُتنبأ به (به أخطاء)
predicted_text = "فكۥۥلۥۥ"
print("المتنبأ به:", predicted_text)

# تحليل الأخطاء
errors = explain_error(
uthmani_text=uthmani_text,
ref_ph_text=ref_out.phonemes,
predicted_ph_text=predicted_text,
mappings=ref_out.mappings,
)

# عرض النتائج
for err in errors:
print("\n" + "="*50)
print(f"الموقع في العثماني: `{uthmani_text[err.uthmani_pos[0]:err.uthmani_pos[1]]}`, {err.uthmani_pos}")
print(f"الموقع في الصوتي: `{ref_out.phonemes[err.ph_pos[0]:err.ph_pos[1]]}`, {err.ph_pos}")
print(f"نوع الخطأ: {err.error_type} - {err.speech_error_type}")
print(f"المتوقع: '{err.expected_ph}' - المُتنبأ به: '{err.preditected_ph}'")
if err.ref_tajweed_rules:
for rule in err.ref_tajweed_rules:
print(f" قاعدة تجويد مرجعية: {rule.name.ar} ({rule.name.en})")
if err.replaced_tajweed_rules:
for rule in err.replaced_tajweed_rules:
print(f" قاعدة تجويد مستبدلة: {rule.name.ar} ({rule.name.en})")
if err.missing_tajweed_rules:
for rule in err.missing_tajweed_rules:
print(f" قاعدة تجويد مفقودة: {rule.name.ar} ({rule.name.en})")
```

**مخرجات متوقعة (Partial output):**
```
المرجع: قَاالُۥۥ
المتنبأ به: فكۥۥلۥۥ

==================================================
الموقع في العثماني: ``, (0, 0)
الموقع في الصوتي: ``, (0, 0)
نوع الخطأ: normal - insert
المتوقع: '' - المُتنبأ به: 'ف'

==================================================
الموقع في العثماني: `قَ`, (0, 2)
الموقع في الصوتي: `قَ`, (0, 2)
نوع الخطأ: normal - replace
المتوقع: 'قَ' - المُتنبأ به: 'ك'

==================================================
الموقع في العثماني: `ا`, (2, 3)
الموقع في الصوتي: `اا`, (2, 4)
نوع الخطأ: tajweed - replace
المتوقع: 'اا' - المُتنبأ به: 'ۥۥ'
قاعدة تجويد مرجعية: المد الطبيعي (Normal Madd)
قاعدة تجويد مستبدلة: المد الطبيعي (Normal Madd)

==================================================
الموقع في العثماني: `لُ`, (3, 5)
الموقع في الصوتي: `لُ`, (4, 6)
نوع الخطأ: tashkeel - delete
المتوقع: 'لُ' - المُتنبأ به: 'ل'
```

---

### 📦 كائنات تحليل الأخطاء (Error Analysis Dataclasses)

#### `ReciterError`
يمثل خطأ واحد في التلاوة:
- `uthmani_pos`: tuple[int, int] – موقع الخطأ في النص العثماني (بداية، نهاية).
- `ph_pos`: tuple[int, int] – موقع الخطأ في النص الصوتي المرجعي (بداية، نهاية).
- `error_type`: Literal["tajweed", "normal", "tashkeel"] – نوع الخطأ.
- `speech_error_type`: Literal["insert", "delete", "replace"] – نوع الخطأ الكلامي.
- `expected_ph`: str – المقطع الصوتي المتوقع.
- `preditected_ph`: str – المقطع الصوتي المُتنبأ به.
- `expected_len`: Optional[int] – الطول المتوقع (لأخطاء المد مثلاً).
- `predicted_len`: Optional[int] – الطول الفعلي.
- `ref_tajweed_rules`: Optional[list[TajweedRule]] – قواعد التجويد المرتبطة بالمقطع المتوقع.
- `inserted_tajweed_rules`, `replaced_tajweed_rules`, `missing_tajweed_rules`: Optional[list[TajweedRule]] – قواعد التجويد التي تم إدراجها أو استبدالها أو فقدانها.


### الحروف: (43)


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
[project]
license = "MIT"
name = "quran-transcript"
version = "0.4.0"
version = "0.5.1"
authors = [
{ name="Abdullah", email="abdullahamlyossef@gmail.com" },
]
Expand Down
Binary file modified quran-script/ph_index.npy
Binary file not shown.
3 changes: 3 additions & 0 deletions src/quran_transcript/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
NoPhonemesSearchResult,
PhoneticSearch,
)
from .phonetics.error_explainer import explain_error, ReciterError

from . import alphabet as alphabet

Expand Down Expand Up @@ -54,4 +55,6 @@
"PhonmesSearhResult",
"NoPhonemesSearchResult",
"PhoneticSearch",
"explain_error",
"ReciterError",
]
63 changes: 60 additions & 3 deletions src/quran_transcript/phonetics/conv_base_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,21 @@ def get_mappings(
# TODO: remove this
assert all(m is not None for m in new_mappings)

# Special Case where we want to assgin the tag for Leen Madd
for m_idx in range(len(new_mappings)):
if new_mappings[m_idx].tajweed_rules:
for taj_idx in range(len(new_mappings[m_idx].tajweed_rules)):
if (
new_mappings[m_idx].tajweed_rules[taj_idx].name.en == "Leen Madd"
and new_mappings[m_idx].tajweed_rules[taj_idx].tag is None
):
tag = (
new_mappings[m_idx]
.tajweed_rules[taj_idx]
._madd_to_tag[new_text[new_mappings[m_idx].pos[0]]]
)
new_mappings[m_idx].tajweed_rules[taj_idx].tag = tag

# Special case where we have Idgham tanween
# Special sympol `tanweed_idgham_detrminer` has no meaning moving it to the tanween
for re_out in re.finditer(f"{alph.uthmani.tanween_idhaam_dterminer}[^$]", text):
Expand Down Expand Up @@ -441,6 +456,34 @@ def get_mappings(

new_mappings = merge_mappings(mappings, new_mappings)

# Special case where skoon sign is repaced with qalalah sign
# We want the qalqlah sign associated with the letter it self not the
# Did not want that but no way to solve exept with this
for re_out in re.finditer(
f"[^{alph.uthmani.ras_haaa}{alph.uthmani.shadda}]({alph.phonetics.qlqla})",
new_text,
):
qlq_idx = re_out.span(1)[0]
char_idx = qlq_idx - 1
# getting skon or shadda idx in the merged mappings
m_idx = 0
for m_idx in range(len(new_mappings)):
if new_mappings[m_idx].pos[0] == qlq_idx:
break
# Avodig the case where we have qalqlah at the end with no (shadda or skonJ)
if new_mappings[m_idx - 1].tajweed_rules is None:
new_mappings[m_idx - 1].pos = (
new_mappings[m_idx - 1].pos[0],
new_mappings[m_idx].pos[1],
)
new_mappings[m_idx - 1].tajweed_rules = new_mappings[m_idx].tajweed_rules
new_mappings[m_idx].pos = (
new_mappings[m_idx].pos[1],
new_mappings[m_idx].pos[1],
)
new_mappings[m_idx].deleted = True
new_mappings[m_idx].tajweed_rules = None

# TODO: remove this
curr_m = None
next_m = None
Expand Down Expand Up @@ -577,7 +620,11 @@ def sub_with_mapping(

@dataclass
class ConversionOperation:
regs: list[tuple[str, str]] | tuple[str, str]
regs: (
list[tuple[str, str, TajweedRule] | tuple[str, str]]
| tuple[str, str, TajweedRule]
| tuple[str, str]
)
arabic_name: str
ops_before: list["ConversionOperation"] | None = None

Expand All @@ -594,8 +641,18 @@ def forward(
moshaf: MoshafAttributes,
mappings: MappingListType | None = None,
) -> tuple[str, MappingListType]:
for input_reg, out_reg in self.regs:
text, mappings = sub_with_mapping(input_reg, out_reg, text, mappings)
for reg in self.regs:
if len(reg) == 2:
input_reg, out_reg = reg
taj_rule = None
elif len(reg) == 3:
input_reg, out_reg, taj_rule = reg
else:
raise ValueError("Invalid Input")

text, mappings = sub_with_mapping(
input_reg, out_reg, text, mappings, tajweed_rule=taj_rule
)
return text, mappings

def apply(
Expand Down
Loading