File size: 9,263 Bytes
010e380 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | import re
from camel_tools.tokenizers.word import simple_word_tokenize
from camel_tools.disambig.mle import MLEDisambiguator
class ArabicGrammarGuard:
def __init__(self):
self.mle = MLEDisambiguator.pretrained()
self.number_words = ["واحد", "اثنان", "اثنين", "ثلاث", "أربع", "خمس", "ست", "سبع", "ثمان", "تسع", "عشر",
"عشرون", "عشرين", "ثلاثون", "ثلاثين", "أربعون", "أربعين", "خمسون", "خمسين",
"ستون", "ستين", "سبعون", "سبعين", "ثمانون", "ثمانين", "تسعون", "تسعين", "مائة", "ألف"]
self.asmaa_khamsa_roots = ['اب', 'اخ', 'حم', 'فو', 'ذو']
def preserve_numbers(self, original_text, generated_text):
orig_digits = re.findall(r'\d+', original_text)
gen_digits = re.findall(r'\d+', generated_text)
if orig_digits and gen_digits and orig_digits != gen_digits:
return original_text
orig_words = [w for w in original_text.split() if any(num in w for num in self.number_words)]
gen_words = [w for w in generated_text.split() if any(num in w for num in self.number_words)]
if len(orig_words) > 0 and len(gen_words) > 0:
if not any(orig[:3] in gen for orig in orig_words for gen in gen_words):
return original_text
return generated_text
def fix_number_and_gender_agreement(self, text):
tokens = simple_word_tokenize(text)
disambig_tokens = self.mle.disambiguate(tokens)
corrected_tokens = list(tokens)
for i in range(len(disambig_tokens) - 1):
w1_info = disambig_tokens[i].analyses[0] if disambig_tokens[i].analyses else None
w2_info = disambig_tokens[i+1].analyses[0] if disambig_tokens[i+1].analyses else None
if not w1_info or not w2_info: continue
w1_pos = w1_info.analysis.get('pos', 'unknown')
w2_pos = w2_info.analysis.get('pos', 'unknown')
w1_word = corrected_tokens[i]
w2_word = corrected_tokens[i+1]
if w1_pos == 'verb' and w2_pos == 'noun':
if (w1_word.endswith('ون') or w1_word.endswith('وا')) and (w2_word.endswith('ون') or w2_word.endswith('ين')):
if w1_word.endswith('ون'): corrected_tokens[i] = w1_word[:-2]
elif w1_word.endswith('وا'): corrected_tokens[i] = w1_word[:-2]
elif w1_pos == 'noun' and w2_pos == 'verb':
if w1_word.endswith('ون') and not (w2_word.endswith('ون') or w2_word.endswith('وا') or w2_word.endswith('ين')):
if w2_info.analysis.get('num') == 's':
corrected_tokens[i+1] = w2_word + 'ون'
# ⚠️ التعديل الجذري هنا: المطابقة للصفات (adj) فقط، ومنع الكلمات التي تبدأ بـ "ب" أو تنتهي بألف التنوين
elif w1_pos == 'noun' and w2_pos == 'adj':
if w1_word.endswith('ون') and not w2_word.endswith('ون'):
if w2_info.analysis.get('num') == 's' and w2_info.analysis.get('gen') == 'm':
if len(w2_word) > 2 and not w2_word.endswith('ا') and not w2_word.startswith('ب'):
corrected_tokens[i+1] = w2_word + 'ون'
return " ".join(corrected_tokens)
def smart_asmaa_khamsa_fix(self, text):
tokens = simple_word_tokenize(text)
disambig_tokens = self.mle.disambiguate(tokens)
corrected_tokens = []
verb_seen = False
for i, token_info in enumerate(disambig_tokens):
word = tokens[i]
pos_tag = token_info.analyses[0].analysis.get('pos', 'unknown') if token_info.analyses else 'unknown'
if pos_tag == 'verb':
verb_seen = True
corrected_tokens.append(word)
continue
is_asmaa = any(word.startswith(root) or word.startswith('أ' + root[1:]) for root in self.asmaa_khamsa_roots if len(root)>1)
if is_asmaa and len(word) >= 3:
if verb_seen:
word = word.replace('ا', 'و').replace('ي', 'و')
verb_seen = False
corrected_tokens.append(word)
return " ".join(corrected_tokens)
def fix_verbs_nasb_and_jazm(self, text):
tokens = simple_word_tokenize(text)
disambig_tokens = self.mle.disambiguate(tokens)
nasb_particles = ['أن', 'لن', 'كي', 'لكي', 'حتى', 'إذن']
jazm_particles = ['لم', 'لما', 'لا']
corrected_tokens = []
for i, token_info in enumerate(disambig_tokens):
word = tokens[i]
pos_tag = token_info.analyses[0].analysis.get('pos', 'unknown') if token_info.analyses else 'unknown'
is_nasb_context = False
is_jazm_context = False
if i > 0:
prev_word = tokens[i-1]
if prev_word in nasb_particles or word.startswith('ل'):
is_nasb_context = True
if prev_word in jazm_particles or word.startswith('ل') or word.startswith('ول'):
is_jazm_context = True
if pos_tag == 'verb' and (is_nasb_context or is_jazm_context):
if word.endswith('ون'):
word = word[:-2] + 'وا'
elif word.endswith('ان'):
word = word[:-2] + 'ا'
elif word.endswith('ين'):
word = word[:-2] + 'ي'
elif is_jazm_context:
if word.endswith('و') and len(word) > 3:
word = word[:-1] + 'ُ'
elif (word.endswith('i') or word.endswith('ي')) and len(word) > 3:
if word.endswith('ي'): word = word[:-1] + 'ِ'
elif (word.endswith('ى') or word.endswith('ا')) and len(word) > 3:
word = word[:-1] + 'َ'
corrected_tokens.append(word)
return " ".join(corrected_tokens)
def fix_gender_agreement(self, text):
text = re.sub(r'\bهذان\s+(ال[أ-ي]+تان)\b', r'هاتان \1', text)
text = re.sub(r'\bهاتان\s+(ال[أ-ي]+[^ت]ان)\b', r'هذان \1', text)
text = re.sub(r'\bهذهن\b', 'هاتان', text)
text = re.sub(r'\bأحد عشر\s+([أ-ي]+ة)\b', r'إحدى عشرة \1', text)
text = re.sub(r'\bأحد عشرة\s+([أ-ي]+ة)\b', r'إحدى عشرة \1', text)
text = re.sub(r'\bإحدى عشرة\s+([أ-ي]+ا|رجل[اأ]|طالب[اأ]|مهندس[اأ])\b', r'أحد عشر \1', text)
text = re.sub(r'\bإحدى عشر\s+([أ-ي]+ا|رجل[اأ]|طالب[اأ]|مهندس[اأ])\b', r'أحد عشر \1', text)
return text
def fix_prepositions_advanced(self, text):
# ⚠️ السماح بحروف العطف (و، ف) قبل حرف الجر
# (في المهندسون) -> (في المهندسين)
text = re.sub(r'\b([وف]?(?:في|من|إلى|على|عن|حتى))\s+([أ-ي]{2,})(ون|ان)\b', r'\1 \2ين', text)
# (وبالمبرمجون) -> (وبالمبرمجين)
text = re.sub(r'\b([وف]?[بلكف])ال([أ-ي]{2,})(ون|ان)\b', r'\1ال\2ين', text)
# (ولمهندسون) -> (ولمهندسين)
text = re.sub(r'\b([وف]?ل)([أ-ي]{2,})(ون|ان)\b', r'\1\2ين', text)
return text
def regex_rules_fallback(self, text):
# إن وأخواتها (كما هي)
text = re.sub(r'\b(إن|أن|كأن|لكن|لعل|ليت)\s+(أبوك|أخوك|ذو|فوك)\b',
lambda m: f"{m.group(1)} {m.group(2).replace('و', 'ا')}", text)
# ⚠️ حروف الجر المنفصلة بمسافة (في أخوك -> في أخيك)
text = re.sub(r'\b([وف]?(?:في|من|إلى|على|عن))\s+(أبوك|أباك|أخوك|أخاك|ذو|ذا)\b',
lambda m: f"{m.group(1)} {m.group(2).replace('و', 'ي').replace('ا', 'ي')}", text)
# ⚠️ حروف الجر المتصلة بدون مسافة (بأخوك، لأبوك -> بأخيك، لأبيك)
text = re.sub(r'\b([وف]?[بل])(أبوك|أباك|أخوك|أخاك|ذو|ذا)\b',
lambda m: f"{m.group(1)}{m.group(2).replace('و', 'ي').replace('ا', 'ي')}", text)
return text
def process(self, original_text, generated_text):
text = self.preserve_numbers(original_text, generated_text)
text = self.fix_number_and_gender_agreement(text)
text = self.smart_asmaa_khamsa_fix(text)
text = self.fix_verbs_nasb_and_jazm(text)
text = self.fix_gender_agreement(text)
text = self.fix_prepositions_advanced(text)
text = self.regex_rules_fallback(text)
text = re.sub(r'\s+', ' ', text).strip()
return text |