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