mohammedahmedezz2004 commited on
Commit
f85a2ba
·
verified ·
1 Parent(s): fd6f6e6

Create Grammer_Rules.py

Browse files
Files changed (1) hide show
  1. Grammer_Rules.py +236 -0
Grammer_Rules.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from camel_tools.tokenizers.word import simple_word_tokenize
3
+ from camel_tools.disambig.mle import MLEDisambiguator
4
+
5
+ class ArabicGrammarGuard:
6
+ def __init__(self):
7
+ # تحميل محلل فك اللبس النحوي وتحديد أقسام الكلام (POS Tagging)
8
+ self.mle = MLEDisambiguator.pretrained()
9
+
10
+ # قائمة الكلمات العددية لحمايتها من الهلوسة
11
+ self.number_words = ["واحد", "اثنان", "اثنين", "ثلاث", "أربع", "خمس", "ست", "سبع", "ثمان", "تسع", "عشر",
12
+ "عشرون", "عشرين", "ثلاثون", "ثلاثين", "أربعون", "أربعين", "خمسون", "خمسين",
13
+ "ستون", "ستين", "سبعون", "سبعين", "ثمانون", "ثمانين", "تسعون", "تسعين", "مائة", "ألف"]
14
+
15
+ # جذور الأسماء الخمسة للتعرف عليها صرفياً
16
+ self.asmaa_khamsa_roots = ['اب', 'اخ', 'حم', 'فو', 'ذو']
17
+
18
+ def preserve_numbers(self, original_text, generated_text):
19
+ """
20
+ 1. حارس الأرقام والحقائق:
21
+ يمنع المودل من تغيير الأرقام الحسابية أو المكتوبة (مثال: من 30 لـ 20).
22
+ لو حصل تغيير جذري، السيستم بيرجع النص الأصلي للمستخدم كـ Fallback لحماية البيانات.
23
+ """
24
+ # فحص الأرقام الحسابية (Digits)
25
+ orig_digits = re.findall(r'\d+', original_text)
26
+ gen_digits = re.findall(r'\d+', generated_text)
27
+ if orig_digits and gen_digits and orig_digits != gen_digits:
28
+ return original_text
29
+
30
+ # فحص الأرقام المكتوبة كحروف
31
+ orig_words = [w for w in original_text.split() if any(num in w for num in self.number_words)]
32
+ gen_words = [w for w in generated_text.split() if any(num in w for num in self.number_words)]
33
+ if len(orig_words) > 0 and len(gen_words) > 0:
34
+ if not any(orig[:3] in gen for orig in orig_words for gen in gen_words):
35
+ return original_text
36
+ return generated_text
37
+
38
+ def fix_number_and_gender_agreement(self, text):
39
+ """
40
+ 2. مطابقة الإفراد والجمع (Agreement):
41
+ تطابق الفعل والفاعل، والمبتدأ والخبر/النعت في حالات جمع المذكر السالم والمثنى القياسية.
42
+ """
43
+ tokens = simple_word_tokenize(text)
44
+ disambig_tokens = self.mle.disambiguate(tokens)
45
+ corrected_tokens = list(tokens)
46
+
47
+ for i in range(len(disambig_tokens) - 1):
48
+ w1_info = disambig_tokens[i].analyses[0] if disambig_tokens[i].analyses else None
49
+ w2_info = disambig_tokens[i+1].analyses[0] if disambig_tokens[i+1].analyses else None
50
+
51
+ if not w1_info or not w2_info:
52
+ continue
53
+
54
+ w1_pos = w1_info.lex.pos
55
+ w2_pos = w2_info.lex.pos
56
+ w1_word = corrected_tokens[i]
57
+ w2_word = corrected_tokens[i+1]
58
+
59
+ # أ. الفعل يسبق الفاعل: الفعل في أول الجملة يلزم الإفراد (ذهبوا المعلمون -> ذهب المعلمون)
60
+ if w1_pos == 'verb' and w2_pos == 'noun':
61
+ if (w1_word.endswith('ون') or w1_word.endswith('وا')) and (w2_word.endswith('ون') or w2_word.endswith('ين')):
62
+ if w1_word.endswith('ون'):
63
+ corrected_tokens[i] = w1_word[:-2]
64
+ elif w1_word.endswith('وا'):
65
+ corrected_tokens[i] = w1_word[:-2]
66
+
67
+ # ب. الاسم يسبق الفعل: الفعل يطابق المبتدأ في الجمع (المهندسون يعمل -> المهندسون يعملون)
68
+ elif w1_pos == 'noun' and w2_pos == 'verb':
69
+ if w1_word.endswith('ون') and not (w2_word.endswith('ون') or w2_word.endswith('وا') or w2_word.endswith('ين')):
70
+ if w2_info.num == 's':
71
+ corrected_tokens[i+1] = w2_word + 'ون'
72
+
73
+ # ج. المبتدأ والخبر / النعت والمنعوت: (المبرمجون ماهر -> المبرمجون ماهرون)
74
+ elif w1_pos == 'noun' and w2_pos in ['noun', 'adj']:
75
+ if w1_word.endswith('ون') and not w2_word.endswith('ون'):
76
+ if w2_info.num == 's' and w2_info.gen == 'm':
77
+ if len(w2_word) > 2:
78
+ corrected_tokens[i+1] = w2_word + 'ون'
79
+
80
+ return " ".join(corrected_tokens)
81
+
82
+ def smart_asmaa_khamsa_fix(self, text):
83
+ """
84
+ 3. الأسماء الخمسة في حالة الرفع (بعد الأفعال):
85
+ تتعرف على الجملة الفعلية وترفع الأسماء الخمسة بالواو إذا وقعت فاعلاً مباشراً.
86
+ """
87
+ tokens = simple_word_tokenize(text)
88
+ disambig_tokens = self.mle.disambiguate(tokens)
89
+ corrected_tokens = []
90
+ verb_seen = False
91
+
92
+ for i, token_info in enumerate(disambig_tokens):
93
+ word = tokens[i]
94
+ pos_tag = token_info.analyses[0].lex.pos if token_info.analyses else 'unknown'
95
+
96
+ if pos_tag == 'verb':
97
+ verb_seen = True
98
+ corrected_tokens.append(word)
99
+ continue
100
+
101
+ is_asmaa = any(word.startswith(root) or word.startswith('أ' + root[1:]) for root in self.asmaa_khamsa_roots if len(root)>1)
102
+
103
+ if is_asmaa and len(word) >= 3:
104
+ if verb_seen:
105
+ # تحويل الكلمة لحالة الرفع بالواو (أخاك/أخيك -> أخوك)
106
+ word = word.replace('ا', 'و').replace('ي', 'و')
107
+ verb_seen = False
108
+
109
+ corrected_tokens.append(word)
110
+
111
+ return " ".join(corrected_tokens)
112
+
113
+ def fix_verbs_nasb_and_jazm(self, text):
114
+ """
115
+ 4. أدوات نصب وجزم الفعل المضارع والأفعال الخمسة:
116
+ تقفش أدوات النصب والجزم، وتطبق حذف النون للأفعال الخمسة، وحذف حرف العلة للمعتل الآخر في الجزم.
117
+ """
118
+ tokens = simple_word_tokenize(text)
119
+ disambig_tokens = self.mle.disambiguate(tokens)
120
+
121
+ nasb_particles = ['أن', 'لن', 'كي', 'لكي', 'حتى', 'إذن']
122
+ jazm_particles = ['لم', 'لما', 'لا']
123
+
124
+ corrected_tokens = []
125
+
126
+ for i, token_info in enumerate(disambig_tokens):
127
+ word = tokens[i]
128
+ pos_tag = token_info.analyses[0].lex.pos if token_info.analyses else 'unknown'
129
+ is_nasb_context = False
130
+ is_jazm_context = False
131
+
132
+ if i > 0:
133
+ prev_word = tokens[i-1]
134
+ if prev_word in nasb_particles or word.startswith('ل'):
135
+ is_nasb_context = True
136
+ if prev_word in jazm_particles or word.startswith('ل') or word.startswith('ول'):
137
+ is_jazm_context = True
138
+
139
+ if pos_tag == 'verb' and (is_nasb_context or is_jazm_context):
140
+ # أ. الأفعال الخمسة (حذف النون)
141
+ if word.endswith('ون'):
142
+ word = word[:-2] + 'وا'
143
+ elif word.endswith('ان'):
144
+ word = word[:-2] + 'ا'
145
+ elif word.endswith('ين'):
146
+ word = word[:-2] + 'ي'
147
+
148
+ # ب. الفعل المعتل الآخر (في الجزم فقط - حذف حرف العلة)
149
+ elif is_jazm_context:
150
+ if word.endswith('و') and len(word) > 3:
151
+ word = word[:-1] + 'ُ'
152
+ elif (word.endswith('i') or word.endswith('ي')) and len(word) > 3:
153
+ if word.endswith('ي'): word = word[:-1] + 'ِ'
154
+ elif (word.endswith('ى') or word.endswith('ا')) and len(word) > 3:
155
+ word = word[:-1] + 'َ'
156
+
157
+ corrected_tokens.append(word)
158
+ return " ".join(corrected_tokens)
159
+
160
+ def fix_gender_agreement(self, text):
161
+ """
162
+ 5. مطابقة التذكير والتأنيث (أسماء الإشارة والأعداد المركبة):
163
+ تمنع الهلوسات اللفظية وتطابق أسماء الإشارة مع المثنى، والأعداد 11 و 12 مع المعدود.
164
+ """
165
+ # تصحيح أسماء الإشارة مع المثنى المؤنث والمذكر
166
+ text = re.sub(r'\bهذان\s+(ال[أ-ي]+تان)\b', r'هاتان \1', text)
167
+ text = re.sub(r'\bهاتان\s+(ال[أ-ي]+[^ت]ان)\b', r'هذان \1', text)
168
+ text = re.sub(r'\bهذهن\b', 'هاتان', text)
169
+
170
+ # مطابقة العدد 11 و 12 مع المؤنث (تاء مربوطة في المعدود)
171
+ text = re.sub(r'\bأحد عشر\s+([أ-ي]+ة)\b', r'إحدى عشرة \1', text)
172
+ text = re.sub(r'\bأحد عشرة\s+([أ-ي]+ة)\b', r'إحدى عشرة \1', text)
173
+
174
+ # مطابقة العدد 11 و 12 مع المذكر
175
+ text = re.sub(r'\bإحدى عشرة\s+([أ-ي]+ا|رجل[اأ]|طالب[اأ]|مهندس[اأ])\b', r'أحد عشر \1', text)
176
+ text = re.sub(r'\bإحدى عشر\s+([أ-ي]+ا|رجل[اأ]|طالب[اأ]|مهندس[اأ])\b', r'أحد عشر \1', text)
177
+ return text
178
+
179
+ def fix_prepositions_advanced(self, text):
180
+ """
181
+ 6. الجار والمجرور المتقدم (المثنى وجمع المذكر السالم):
182
+ تجر بالياء والنون (ين) الكلمات المجرورة بحروف منفصلة أو متصلة (بـ، لـ، كـ، فـ).
183
+ """
184
+ # حروف الجر المنفصل�� (في المهندسون -> في المهندسين)
185
+ text = re.sub(r'\b(في|من|إلى|على|عن|حتى)\s+([أ-ي]{2,})(ون|ان)\b', r'\1 \2ين', text)
186
+
187
+ # حروف الجر المتصلة بـ ال التعريف (بالمهندسون -> بالمعلمين)
188
+ text = re.sub(r'\b([بلكف])ال([أ-ي]{2,})(ون|ان)\b', r'\1ال\2ين', text)
189
+
190
+ # حرف اللام المتصل مباشرة بدون ال (لمهندسون -> لمهندسين)
191
+ text = re.sub(r'\bل([أ-ي]{2,})(ون|ان)\b', r'ل\1ين', text)
192
+ return text
193
+
194
+ def regex_rules_fallback(self, text):
195
+ """
196
+ 7. القواعد الهيكلية بالـ Regex للحالات المباشرة:
197
+ تتعامل مع إن وأخواتها وحروف الجر المباشرة مع الأسماء الخمسة.
198
+ """
199
+ # إن وأخواتها تنصب الأسماء الخمسة بالألف (إن أبوك -> إن أباك)
200
+ text = re.sub(r'\b(إن|أن|كأن|لكن|لعل|ليت)\s+(أبوك|أخوك|ذو|فوك)\b',
201
+ lambda m: f"{m.group(1)} {m.group(2).replace('و', 'ا')}", text)
202
+
203
+ # حروف الجر تجر الأسماء الخمسة بالياء (إلى أبوك -> إلى أخيك)
204
+ text = re.sub(r'\b(في|من|إلى|على|عن|بـ|لـ)\s+(أبوك|أباك|أخوك|أخاك|ذو|ذا)\b',
205
+ lambda m: f"{m.group(1)} {m.group(2).replace('و', 'ي').replace('ا', 'ي')}", text)
206
+ return text
207
+
208
+ def process(self, original_text, generated_text):
209
+ """
210
+ (Pipeline Execution):
211
+ تستقبل النص الأصلي والخرّوج المستلم من الـ AI، وتمشيهم بترتيب هندسي صارم.
212
+ """
213
+ # خطوة 1: حماية الحقائق والأرقام
214
+ text = self.preserve_numbers(original_text, generated_text)
215
+
216
+ # خطوة 2: المطابقة التناغمية للإفراد والجمع
217
+ text = self.fix_number_and_gender_agreement(text)
218
+
219
+ # خطوة 3: معالجة رفع الفاعل في الأسماء الخمسة
220
+ text = self.smart_asmaa_khamsa_fix(text)
221
+
222
+ # خطوة 4: ضبط النصب والجزم للأفعال
223
+ text = self.fix_verbs_nasb_and_jazm(text)
224
+
225
+ # خطوة 5: ضبط التذكير والتأنيث
226
+ text = self.fix_gender_agreement(text)
227
+
228
+ # خطوة 6: ضبط الجار والمجرور المتقدم (المتصل والمنفصل)
229
+ text = self.fix_prepositions_advanced(text)
230
+
231
+ # خطوة 7: تطبيق رولز الـ Regex الهيكلية لإن وأخواتها والجر
232
+ text = self.regex_rules_fallback(text)
233
+
234
+ # تنظيف الفراغات الناتجة عن المعالجة
235
+ text = re.sub(r'\s+', ' ', text).strip()
236
+ return text