youssefreda9 commited on
Commit
fa8b3c4
·
1 Parent(s): e383e71

chore: remove large db file from git (HF 10MB limit)

Browse files
Files changed (2) hide show
  1. quran.py +0 -350
  2. quran_master.db +0 -3
quran.py DELETED
@@ -1,350 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:8249c308936ddc26e70e46580405f7751d2c96501d1185f617ae9b0436e987a9
3
- size 23064576