Abdo0431 commited on
Commit
20bd4e4
·
verified ·
1 Parent(s): 5a74d5f

Upload 2 files

Browse files
Files changed (3) hide show
  1. .gitattributes +1 -0
  2. quran.py +350 -0
  3. quran_master.db +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ quran_master.db filter=lfs diff=lfs merge=lfs -text
quran.py ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """Quran.py
3
+
4
+ Automatically generated by Colab.
5
+
6
+ Original file is located at
7
+ https://colab.research.google.com/drive/1WwaR-xsFnY5iffCndJV5metzB0RS4_GP
8
+ """
9
+
10
+ import sqlite3
11
+ import re
12
+ from rapidfuzz.distance import Levenshtein
13
+
14
+ # ==========================================
15
+ # طبقة التطبيع الموسعة
16
+ # ==========================================
17
+
18
+ # مجموعات الحروف المتشابهة بصرياً أو صوتياً — كلها ترجع لشكل واحد
19
+ _SIMILAR_GROUPS = [
20
+ ('اأإآٱ', 'ا'),
21
+ ('ةه', 'ه'),
22
+ ('يىئ', 'ي'),
23
+ ('وؤ', 'و'),
24
+ ('ذد', 'د'), # متقاربة بصرياً للمبتدئين
25
+ ('زرذ', 'ر'), # أحياناً يخلط المستخدم بينها
26
+ ('طت', 'ت'),
27
+ ('ضظ', 'ض'),
28
+ ('سص', 'س'),
29
+ ('ثت', 'ت'),
30
+ ('خح', 'ح'),
31
+ ('غع', 'ع'),
32
+ ]
33
+
34
+ def _build_similarity_table():
35
+ table = {}
36
+ for group, canonical in _SIMILAR_GROUPS:
37
+ for ch in group:
38
+ table[ch] = canonical
39
+ return table
40
+
41
+ _SIM_TABLE = _build_similarity_table()
42
+
43
+ def normalize_arabic(text: str, *, deep: bool = False) -> str:
44
+ """
45
+ تطبيع النص العربي.
46
+ deep=False → تطبيع خفيف (للتخزين في DB أو المقارنة الدقيقة)
47
+ deep=True → تطبيع عميق يساوي بين الحروف المتشابهة (للبحث الضبابي)
48
+ """
49
+ if not text:
50
+ return ""
51
+ # إزالة التشكيل والعلامات القرآنية
52
+ text = re.sub(
53
+ r'[\u064B-\u065F\u0670\u0671\u0656'
54
+ r'\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED]',
55
+ '', text
56
+ )
57
+ # همزات
58
+ text = re.sub(r'[أإآٱ]', 'ا', text)
59
+ # تاء مربوطة
60
+ text = re.sub(r'ة', 'ه', text)
61
+ # ألف مقصورة
62
+ text = re.sub(r'ى', 'ي', text)
63
+
64
+ if deep:
65
+ text = ''.join(_SIM_TABLE.get(ch, ch) for ch in text)
66
+
67
+ return ' '.join(text.strip().split())
68
+
69
+
70
+ # ==========================================
71
+ # مطابقة ضبابية على مستوى الكلمات
72
+ # ==========================================
73
+
74
+ def _word_similarity(a: str, b: str) -> float:
75
+ """نسبة التشابه بين كلمتين (0.0 → 1.0)."""
76
+ max_len = max(len(a), len(b), 1)
77
+ dist = Levenshtein.distance(a, b)
78
+ return 1.0 - dist / max_len
79
+
80
+
81
+ def _score_window(query_words: list[str], window_words: list[str],
82
+ query_deep: list[str], window_deep: list[str]) -> float:
83
+ """
84
+ احسب نسبة تطابق نافذة كلمات مع استعلام المستخدم.
85
+ نستخدم نسختين: خفيفة (أولوية) وعميقة (احتياط).
86
+ """
87
+ if len(window_words) != len(query_words):
88
+ return 0.0
89
+
90
+ total = 0.0
91
+ for qw, ww, qd, wd in zip(query_words, window_words, query_deep, window_deep):
92
+ # نأخذ أعلى نتيجة بين المقارنة الخفيفة والعميقة
93
+ s_light = _word_similarity(qw, ww)
94
+ s_deep = _word_similarity(qd, wd)
95
+ total += max(s_light, s_deep)
96
+
97
+ return total / len(query_words)
98
+
99
+
100
+ # ==========================================
101
+ # الدالة الرئيسية
102
+ # ==========================================
103
+
104
+ def search_bayan(query_text: str,
105
+ target_type: str = "تدقيق الايات",
106
+ fuzzy_threshold: float = 0.72) -> dict:
107
+ """
108
+ البحث عن آية قرآنية مع دعم الأخطاء الإملائية.
109
+
110
+ المعاملات:
111
+ query_text : النص المُدخَل من المستخدم (قد يحتوي أخطاء)
112
+ target_type : لغة الإخراج (uthmani / english / french / ...)
113
+ fuzzy_threshold : الحد الأدنى لقبول التطابق (0→1)، افتراضياً 0.72
114
+
115
+ المُخرج:
116
+ dict يحتوي على:
117
+ matched_segment : النص المُصحَّح بالرسم العثماني أو الترجمة
118
+ full_verse : الآيات كاملة مع التوثيق
119
+ similarity_score : درجة التشابه
120
+ metadata : تفاصيل الآيات
121
+ أو:
122
+ error : رسالة الخطأ
123
+ """
124
+ conn = sqlite3.connect('quran_master.db')
125
+ cursor = conn.cursor()
126
+
127
+ language_mapping = {
128
+ "تدقيق الايات": "uthmani",
129
+ "bengali": "bn", "bosnian": "bs", "english": "en", "french": "fr",
130
+ "german": "de", "indonesian": "id", "malay": "ms", "persian": "fa",
131
+ "portuguese": "pt", "russian": "ru", "spanish": "es",
132
+ "turkish": "tr", "uzbek": "uz"
133
+ }
134
+
135
+ clean_target = str(target_type).lower().strip()
136
+ lang_code = language_mapping.get(clean_target, "uthmani")
137
+
138
+ verse_column = "v.text_uthmani" if lang_code == "uthmani" else f"v.lang_{lang_code}"
139
+ sura_column = "s.ar" if lang_code == "uthmani" else f"s.lang_{lang_code}"
140
+
141
+ # ── تطبيع الاستعلام بنسختين ──
142
+ query_light = normalize_arabic(query_text, deep=False).split()
143
+ query_deep = normalize_arabic(query_text, deep=True).split()
144
+
145
+ if not query_light:
146
+ conn.close()
147
+ return {"error": "النص المُدخل فارغ"}
148
+
149
+ n = len(query_light)
150
+
151
+ # ==========================================
152
+ # البحث بالمرساة الديناميكية (من الأطول للأقصر)
153
+ # ==========================================
154
+ candidate_starts: list[tuple[int, int]] = []
155
+
156
+ for i in range(n, 0, -1):
157
+ anchor = ' '.join(query_light[:i])
158
+ # بحث LIKE عادي أولاً (سريع)
159
+ cursor.execute("""
160
+ SELECT v.sura_num, v.aya_num
161
+ FROM verses v
162
+ WHERE v.text_clean LIKE ?
163
+ ORDER BY v.sura_num, v.aya_num
164
+ """, ('%' + anchor + '%',))
165
+ candidate_starts = cursor.fetchall()
166
+ if candidate_starts:
167
+ break
168
+
169
+ # إذا لم تجد شيئاً بالمرساة الخفيفة → جرّب المرساة العميقة
170
+ # (يستخدم text_deep إذا كان موجوداً، وإلا يعود لـ text_clean)
171
+ if not candidate_starts:
172
+ # اكتشف أعمدة الجدول
173
+ cursor.execute("PRAGMA table_info(verses)")
174
+ cols = {row[1] for row in cursor.fetchall()}
175
+ deep_col = "v.text_deep" if "text_deep" in cols else "v.text_clean"
176
+
177
+ for i in range(n, 0, -1):
178
+ anchor_deep = ' '.join(query_deep[:i])
179
+ cursor.execute(f"""
180
+ SELECT v.sura_num, v.aya_num
181
+ FROM verses v
182
+ WHERE {deep_col} LIKE ?
183
+ ORDER BY v.sura_num, v.aya_num
184
+ """, ('%' + anchor_deep + '%',))
185
+ rows = cursor.fetchall()
186
+ if rows:
187
+ candidate_starts = rows
188
+ break
189
+
190
+ # الملاذ الأخير: ابحث بكل كلمة على حدة وخذ الآيات الأكثر تكراراً
191
+ if not candidate_starts:
192
+ counts: dict[tuple, int] = {}
193
+ for word in query_light:
194
+ if len(word) < 3:
195
+ continue
196
+ cursor.execute("""
197
+ SELECT v.sura_num, v.aya_num
198
+ FROM verses v
199
+ WHERE v.text_clean LIKE ?
200
+ """, ('%' + word + '%',))
201
+ for row in cursor.fetchall():
202
+ counts[row] = counts.get(row, 0) + 1
203
+ if counts:
204
+ candidate_starts = sorted(counts, key=counts.get, reverse=True)[:15]
205
+
206
+ if not candidate_starts:
207
+ conn.close()
208
+ return {
209
+ "matched_segment": "",
210
+ "full_verse": "لم يُعثر على تطابق — تحقق من النص المُدخل"
211
+ }
212
+
213
+ # ==========================================
214
+ # النافذة المنزلقة + التقييم الضبابي
215
+ # ==========================================
216
+ best_score = -1.0
217
+ best_match_idx = -1
218
+ best_rows = None
219
+
220
+ for start_sura, start_aya in candidate_starts:
221
+ cursor.execute(f"""
222
+ SELECT v.sura_num, v.aya_num, v.text_clean,
223
+ v.text_uthmani, {verse_column}, {sura_column}
224
+ FROM verses v
225
+ JOIN suras_translated s ON v.sura_num = s.sura_number
226
+ WHERE (v.sura_num = ? AND v.aya_num >= ?) OR (v.sura_num > ?)
227
+ ORDER BY v.sura_num, v.aya_num
228
+ LIMIT 12
229
+ """, (start_sura, start_aya, start_sura))
230
+ fetched = cursor.fetchall()
231
+
232
+ QURAN_MARKS = {
233
+ 'ۖ', 'ۗ', 'ۘ', 'ۙ', 'ۚ', 'ۛ', 'ۜ', '۝',
234
+ '۞', '۩'
235
+ }
236
+
237
+
238
+ # بناء خريطة الكلمات
239
+ combined_light, combined_deep, word_map = [], [], []
240
+ for row in fetched:
241
+ s_num, a_num, t_clean, t_uthmani, t_target, s_name = row
242
+ clean_w = t_clean.split()
243
+ uthmani_w = [
244
+ token
245
+ for token in t_uthmani.split()
246
+ if token not in QURAN_MARKS
247
+ ]
248
+ deep_w = normalize_arabic(t_clean, deep=True).split()
249
+
250
+ for j, cw in enumerate(clean_w):
251
+ combined_light.append(cw)
252
+ combined_deep.append(deep_w[j] if j < len(deep_w) else cw)
253
+ word_map.append({
254
+ "clean": cw,
255
+ "uthmani": uthmani_w[j] if j < len(uthmani_w) else cw,
256
+ "sura_num": s_num,
257
+ "aya_num": a_num,
258
+ "target_text": t_target,
259
+ "sura_name": s_name,
260
+ })
261
+
262
+ total_words = len(combined_light)
263
+ if total_words < n:
264
+ continue
265
+
266
+ # نافذة منزلقة — ابحث عن أعلى نتيجة
267
+ for j in range(total_words - n + 1):
268
+ score = _score_window(
269
+ query_light, combined_light[j:j+n],
270
+ query_deep, combined_deep[j:j+n]
271
+ )
272
+ if score > best_score:
273
+ best_score = score
274
+ best_match_idx = j
275
+ best_rows = word_map
276
+
277
+ # إذا وجدنا تطابقاً كاملاً، لا داعي للاستمرار
278
+ if best_score >= 0.999:
279
+ break
280
+
281
+ conn.close()
282
+
283
+ # if best_score < fuzzy_threshold or best_match_idx == -1:
284
+ # return {
285
+ # "error": (
286
+ # f"أقرب تطابق وجدناه بدرجة {best_score:.0%} وهي أقل من الحد المقبول "
287
+ # f"({fuzzy_threshold:.0%}). تحقق من النص المُدخل."
288
+ # )
289
+ # }
290
+
291
+ # ==========================================
292
+ # تشكيل المخرجات
293
+ # ==========================================
294
+ matched_words = best_rows[best_match_idx: best_match_idx + n]
295
+
296
+ # بناء matched_segment مع رقم كل آية
297
+ aya_words: dict[tuple, list] = {}
298
+ for w in matched_words:
299
+ key = (w["sura_num"], w["aya_num"])
300
+ if key not in aya_words:
301
+ aya_words[key] = []
302
+ aya_words[key].append(w["uthmani"] if lang_code == "uthmani" else w["target_text"])
303
+
304
+ if lang_code == "uthmani":
305
+ seg_parts = [
306
+ " ".join(words) + f" ({a_num})"
307
+ for (_, a_num), words in aya_words.items()
308
+ ]
309
+ matched_segment = " ".join(seg_parts)
310
+ else:
311
+ # للترجمات: نص الآية كامل + رقمها (بدون تكرار)
312
+ seen_texts, seg_parts = set(), []
313
+ for (_, a_num), words in aya_words.items():
314
+ txt = words[0] # target_text مكرر لكل كلمة، نأخذ الأول
315
+ if txt not in seen_texts:
316
+ seen_texts.add(txt)
317
+ seg_parts.append(f"{txt} ({a_num})")
318
+ matched_segment = " ".join(seg_parts)
319
+
320
+ # الآيات المشمولة — مرتبة بالترتيب
321
+ involved: dict[tuple, dict] = {}
322
+ for w in matched_words:
323
+ key = (w["sura_num"], w["aya_num"])
324
+ if key not in involved:
325
+ involved[key] = {"sura_name": w["sura_name"], "target_text": w["target_text"]}
326
+
327
+ ayah_nums = [a_num for (_, a_num) in involved]
328
+ sura_name = next(iter(involved.values()))["sura_name"]
329
+
330
+ verse_body_parts = []
331
+ for (s_num, a_num), data in involved.items():
332
+ verse_body_parts.append(f"{data['target_text']} ({a_num})")
333
+
334
+ combined_body = " ".join(verse_body_parts)
335
+
336
+ if len(ayah_nums) == 1:
337
+ ref = f"{sura_name}: {ayah_nums[0]}"
338
+ else:
339
+ nums_str = "،".join(str(x) for x in ayah_nums)
340
+ ref = f"{sura_name}: {nums_str}"
341
+
342
+ full_verse_formatted = f"({combined_body}) [{ref}]"
343
+ matched_segment = f"({matched_segment}) [{ref}]"
344
+
345
+ is_exact = best_score >= 0.999
346
+ return {
347
+ "matched_segment": matched_segment,
348
+ "full_verse": full_verse_formatted,
349
+
350
+ }
quran_master.db ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8249c308936ddc26e70e46580405f7751d2c96501d1185f617ae9b0436e987a9
3
+ size 23064576