bayan-api / diff_gram.txt
youssefreda9's picture
HF Deploy: Fix syntax error with smart quotes in popup.js
fe1e225
Raw
History Blame Contribute Delete
17.7 kB
diff --git a/src/nlp/grammar/grammar_rules.py b/src/nlp/grammar/grammar_rules.py
index 141fb50..1f80154 100644
--- a/src/nlp/grammar/grammar_rules.py
+++ b/src/nlp/grammar/grammar_rules.py
@@ -77,31 +77,8 @@ class ArabicGrammarGuard:
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)
+ # Bug 1.1: Disabled blind replacement of ا and ي with و as it corrupts object position
+ return text
def _apply_jazm_to_verb(self, word, token_info):
# 1. Handle Af'al Khamsa using camel_tools analysis
@@ -182,7 +159,7 @@ class ArabicGrammarGuard:
disambig_tokens = self.mle.disambiguate(tokens)
nasb_particles = ['أن', 'ان', 'لن', 'كي', 'لكي', 'حتى', 'حتي', 'إذن', 'اذا']
- jazm_particles = ['لم', 'لما', 'لا']
+ jazm_particles = ['لم', 'لما']
corrected_tokens = []
@@ -196,12 +173,16 @@ class ArabicGrammarGuard:
if i > 0:
prev_word = tokens[i-1]
- if prev_word in nasb_particles or word.startswith('ل'):
+ if prev_word in nasb_particles:
is_nasb_context = True
- if prev_word in jazm_particles or word.startswith('ل') or word.startswith('ول'):
+ if prev_word in jazm_particles:
is_jazm_context = True
+ # Lam Al-Ta'leel / Lam Al-Amr logic was flawed (triggering for ANY word starting with ل)
+ # Ensure it only applies to actual verbs starting with 'لي' or 'لت' etc.
+ if (word.startswith('ل') or word.startswith('ول') or word.startswith('فل')) and len(word) >= 4 and word[1] in ['ي', 'ت', 'ن', 'أ']:
+ is_nasb_context = True
- is_present_tense = word.startswith('ي') or word.startswith('ت') or word.startswith('ن') or word.startswith('أ')
+ is_present_tense = word.startswith('ي') or word.startswith('ت') or word.startswith('ن') or word.startswith('أ') or word.startswith('لي') or word.startswith('لت') or word.startswith('ولي') or word.startswith('فلي')
if (pos_tag == 'verb' or is_present_tense) and (is_nasb_context or is_jazm_context):
if is_jazm_context:
word = self._apply_jazm_to_verb(word, token_info)
@@ -223,9 +204,14 @@ class ArabicGrammarGuard:
# When a feminine noun is followed by a masculine adjective, add ة
# e.g. السيارة جميل → السيارة جميلة
words = text.split()
+ KNOWN_MASC_TA_MARBUTA = {'خليفة', 'أسامة', 'حمزة', 'طلحة', 'معاوية', 'عبيدة', 'قضاة', 'دعاة', 'رماة', 'حماة'}
for i in range(len(words) - 1):
noun = words[i]
adj = words[i + 1]
+ # Strip prefixes like 'ال' to check root
+ clean_noun = noun[2:] if noun.startswith('ال') else noun
+ if clean_noun in KNOWN_MASC_TA_MARBUTA:
+ continue
is_fem_noun = (noun in KNOWN_FEMININE_NOUNS or
(noun.endswith('ة') and len(noun) >= 3) or
(noun.startswith('ال') and noun.endswith('ة')))
@@ -250,6 +236,9 @@ class ArabicGrammarGuard:
'النيران', 'نيران', 'الألوان', 'ألوان', 'البلدان', 'بلدان',
'الأوطان', 'أوطان', 'الأبدان', 'أبدان', 'الأركان', 'أركان',
'الفرسان', 'فرسان', 'الغزلان', 'غزلان', 'القضبان', 'قضبان',
+ 'فرعون', 'قانون', 'القانون', 'كانون', 'قارون', 'طاعون',
+ 'عربون', 'هارون', 'زيدون', 'معجون', 'مجنون', 'زيتون', 'صابون',
+ 'الفرعون', 'قوانين'
}
def fix_prepositions_advanced(self, text):
@@ -261,31 +250,36 @@ class ArabicGrammarGuard:
stem = m.group(2)
suffix = m.group(3)
full_word = stem + suffix
- # Skip words in blocklist (root nouns, not duals)
- if full_word in self._PREP_BLOCKLIST:
- return m.group(0) # return unchanged
- # Skip ال-prefixed words ending in ان — almost always root nouns
- if stem.startswith('ال') and suffix == 'ان':
- return m.group(0) # return unchanged
- return f'{prep} {stem}ين'
-
- text = re.sub(r'\b([وف]?(?:في|من|إلى|على|عن|حتى))\s+([أ-ي]{4,})(ون|ان)\b', _prep_replace, text)
+
+ # Use camel-tools disambiguation to determine if it's really a dual/plural
+ tokens = simple_word_tokenize(full_word)
+ disambig_tokens = self.mle.disambiguate(tokens)
+ if disambig_tokens and disambig_tokens[0].analyses:
+ num = disambig_tokens[0].analyses[0].analysis.get('num', 's')
+ # Only apply ين suffix if the word is actually Dual or Plural
+ if num in ['d', 'p']:
+ return f'{prep} {stem}ين'
+ return m.group(0)
+
+ text = re.sub(r'\b([وف]?(?:في|من|إلى|على|عن|حتى))\s+([أ-ي]{3,})(ون|ان)\b', _prep_replace, text)
# (وبالمبرمجون) -> (وبالمبرمجين)
# FIX-33b: Same blocklist protection as first regex
def _attached_prep_replace(m):
- prefix = m.group(1) # وب، ب، فب، ول، ل، etc.
+ prefix = m.group(1)
stem = m.group(2)
suffix = m.group(3)
- full_word = 'ال' + stem + suffix # reconstruct with ال for blocklist check
- if full_word in self._PREP_BLOCKLIST:
- return m.group(0)
- # Words ending in ان with 4+ char stems are almost always root nouns
- if suffix == 'ان':
- return m.group(0)
- return f'{prefix}ال{stem}ين'
+ full_word = 'ال' + stem + suffix
+
+ tokens = simple_word_tokenize(full_word)
+ disambig_tokens = self.mle.disambiguate(tokens)
+ if disambig_tokens and disambig_tokens[0].analyses:
+ num = disambig_tokens[0].analyses[0].analysis.get('num', 's')
+ if num in ['d', 'p']:
+ return f'{prefix}ال{stem}ين'
+ return m.group(0)
- text = re.sub(r'\b([وف]?[بلكف])ال([أ-ي]{4,})(ون|ان)\b', _attached_prep_replace, text)
+ text = re.sub(r'\b([وف]?[بلكف])ال([أ-ي]{3,})(ون|ان)\b', _attached_prep_replace, text)
# (ولمهندسون) -> (ولمهندسين)
# FIX-33b: Same protection — reconstruct full word for blocklist
@@ -321,8 +315,8 @@ class ArabicGrammarGuard:
if word == 'ذو': return 'ذا'
if word == 'فو': return 'فا'
if word == 'حمو': return 'حما'
- if word.endswith('ون'): return word[:-2] + 'ين'
- if word.endswith('ان'): return word[:-2] + 'ين'
+ if word.endswith('ون') and num == 'p' and word not in ('قانون', 'فرعون', 'كانون', 'معجون', 'طاعون', 'مجنون'): return word[:-2] + 'ين'
+ if word.endswith('ان') and num == 'd' and word not in ('امتحان', 'إنسان', 'ميدان', 'سلطان', 'شيطان'): return word[:-2] + 'ين'
elif target_case == 'n': # Marfoo'
if word in ('أبا', 'أبي'): return 'أبو'
if word in ('أخا', 'أخي'): return 'أخو'
@@ -481,30 +475,18 @@ class ArabicGrammarGuard:
# FIX-08: Expanded feminine plurals
'المهندسات', 'مهندسات', 'الطبيبات', 'طبيبات',
'اللاعبات', 'لاعبات', 'الممثلات', 'ممثلات',
- 'الشركات', 'شركات', 'الجامعات', 'جامعات',
- 'المدارس', 'مدارس', 'المستشفيات', 'مستشفيات',
- 'الحكومات', 'حكومات', 'المنظمات', 'منظمات',
- 'الطائرات', 'طائرات', 'السيارات', 'سيارات',
}
- if noun_word in KNOWN_PLURALS_MASC:
+ if noun_num == 'd':
+ pass # Dual noun, skip plural logic to prevent corruption
+ elif noun_word in KNOWN_PLURALS_MASC:
is_plural_masc = True
elif noun_word in KNOWN_PLURALS_FEM:
is_plural_fem = True
- elif noun_word.endswith('ون') or noun_word.endswith('ين'):
- # Sound masculine plural — but only if 4+ chars (avoid short words)
- if len(noun_word) >= 5:
- is_plural_masc = True
- elif noun_word.endswith('ات') and len(noun_word) >= 5:
- is_plural_fem = True
- # FIX-08: Broken plural heuristic — common patterns
- elif noun_num == 'p':
- # Trust POS tagger when it says plural AND word is long enough
- if len(noun_word) >= 4:
- if noun_gen == 'f':
- is_plural_fem = True
- else:
- is_plural_masc = True
+ elif (noun_word.endswith('ون') or (noun_word.endswith('ين') and noun_num == 'p')) and len(noun_word) >= 5:
+ # Bug 1.14: Check noun_num == 'p' to prevent duals from triggering plural logic
+ is_plural_masc = True
+ # Bug 1.2: Disabled POS tagger heuristic for broken plurals and generic 'ات' ending because it forces 'ن' on non-human plurals
is_singular_fem = False
if not is_plural_masc and not is_plural_fem:
@@ -574,7 +556,8 @@ class ArabicGrammarGuard:
return word
# إن وأخواتها
- text = re.sub(r'\b(إن|أن|كأن|لكن|لعل|ليت|ان|كان)\s+(أبوك|ابوك|أخوك|اخوك|ذو|فوك)\b',
+ # Bug 1.13: removed كان from this regex because it caused كان أخوك -> كان أخاك
+ text = re.sub(r'\b(إن|أن|كأن|لكن|لعل|ليت|ان)\s+(أبوك|ابوك|أخوك|اخوك|ذو|فوك)\b',
lambda m: f"{m.group(1)} {_add_hamza(m.group(2)).replace('و', 'ا')}", text)
# الأفعال المتعدية (Object position)
@@ -596,19 +579,19 @@ class ArabicGrammarGuard:
# FIX-PC010: Add targeted safe regex for Nasb/Jazm particles + verb
# Only match clear present tense verbs starting with ي/ت/ن/أ and ending in ون
- text = re.sub(r'\b(أن|ان|لن|كي|حتى|لم|لما)\s+([يتا][\u0600-\u06FF]{2,})ون\b',
+ text = re.sub(r'\b(أن|ان|لن|كي|حتى|لم|لما)\s+([يتاأن][\u0600-\u06FF]{2,})ون\b',
r'\1 \2وا', text)
return text
def fix_conditional_sentences(self, text):
- conditional_particles = {'إن', 'ان', 'من', 'ما', 'متى', 'متي', 'مهما', 'أينما', 'حيثما', 'أيان', 'ايان', 'كيفما', 'أنى', 'اني'}
+ conditional_particles = {'إن', 'ان', 'متى', 'متي', 'مهما', 'أينما', 'حيثما', 'أيان', 'ايان', 'كيفما', 'أنى', 'اني'}
tokens = simple_word_tokenize(text)
disambig_tokens = self.mle.disambiguate(tokens)
corrected_tokens = list(tokens)
# Lookahead for 2nd person context
- has_2nd_person_context = any(t.endswith('كم') or t.endswith('كمو') or t.startswith('ت') for t in tokens)
+ has_2nd_person_context = False
in_cond = False
verbs_jazmed = 0
@@ -634,10 +617,6 @@ class ArabicGrammarGuard:
# Apply jazm using the comprehensive camel_tools helper
word = self._apply_jazm_to_verb(word, token_info)
- # Fix pronoun mismatch if 2nd person context exists
- if has_2nd_person_context and word.startswith('ي') and (word.endswith('وا') or word.endswith('ا') or word.endswith('ي')):
- word = 'ت' + word[1:]
-
corrected_tokens[i] = word
# Increment jazmed verbs counter (handles both فعل الشرط and جواب الشرط)
verbs_jazmed += 1
@@ -714,8 +693,8 @@ class ArabicGrammarGuard:
else:
corrected_tokens[i+1] = base_adj + ('ان' if is_nom else 'ين')
- # Plural Human Adjective Agreement
- elif w1_num == 'p' and w1_pos in ['noun', 'unknown']:
+ # Plural Human Adjective Agreement (Sound Plurals Only)
+ elif w1_num == 'p' and w1_pos in ['noun', 'unknown'] and (w1.endswith('ون') or w1.endswith('ين') or w1.endswith('ات')):
base_adj = None
for suffix in ['ان', 'ين', 'تان', 'تين', 'ة', 'ون', 'ات', 'ين', '']:
stem = w2[:-len(suffix)] if suffix else w2
@@ -726,9 +705,11 @@ class ArabicGrammarGuard:
if base_adj:
if w1.endswith('ون') or w1.endswith('ين') or w1_gen == 'm':
is_nom = w1.endswith('ون')
- corrected_tokens[i+1] = base_adj + ('ون' if is_nom else 'ين')
+ if not w2.endswith('ان') and not w2.endswith('ين') and not w2.endswith('ات'):
+ corrected_tokens[i+1] = base_adj + ('ون' if is_nom else 'ين')
elif w1.endswith('ات') or w1_gen == 'f':
- corrected_tokens[i+1] = base_adj + 'ات'
+ if not w2.endswith('ان') and not w2.endswith('ين') and not w2.endswith('ات'):
+ corrected_tokens[i+1] = base_adj + 'ات'
return " ".join(corrected_tokens)
@@ -866,8 +847,9 @@ class ArabicGrammarGuard:
}
words = text.split()
for i, w in enumerate(words):
- if w in _ALWAYS_TANWEEN:
- words[i] = _ALWAYS_TANWEEN[w]
+ clean_w = w.rstrip('.,،؛;:!؟?()[]{}«»"\'…')
+ if clean_w in _ALWAYS_TANWEEN:
+ words[i] = _ALWAYS_TANWEEN[clean_w] + w[len(clean_w):]
return ' '.join(words)
def fix_initial_hamza(self, text):
@@ -920,16 +902,27 @@ class ArabicGrammarGuard:
_PRONOUN_SUFFIXES = {'ه', 'ها', 'ك', 'كم', 'كن', 'هم', 'هن', 'ني', 'نا'}
words = text.split()
for i, w in enumerate(words):
- if w in _HAMZA_FIXES:
- words[i] = _HAMZA_FIXES[w]
+ clean_w = w.rstrip('.,،؛;:!؟?()[]{}«»"\'…')
+ if clean_w in _HAMZA_FIXES:
+ words[i] = _HAMZA_FIXES[clean_w] + w[len(clean_w):]
continue
# إنّ/أنّ: kasra at sentence start, fathah mid-sentence
_is_sent_start = (i == 0) or (words[i-1][-1] in '.؟!؛' if words[i-1] else False)
- if _is_sent_start and w in _INNA_SENTENCE_INITIAL:
- words[i] = _INNA_SENTENCE_INITIAL[w]
+ if _is_sent_start and clean_w in _INNA_SENTENCE_INITIAL:
+ words[i] = _INNA_SENTENCE_INITIAL[clean_w] + w[len(clean_w):]
continue
- if not _is_sent_start and w in _ANNA_MID_SENTENCE:
- words[i] = _ANNA_MID_SENTENCE[w]
+ if not _is_sent_start and clean_w in _ANNA_MID_SENTENCE:
+ if clean_w == 'ان':
+ # Bug 1.5: Added تقول and all variants to Qawl check
+ if i > 0 and words[i-1].rstrip('.,،؛;:!؟?()[]{}«»"\'…') in ('قال', 'قالت', 'يقول', 'يقولون', 'قلت', 'قلنا', 'تقول', 'يقولوا'):
+ words[i] = 'إن' + w[len(clean_w):]
+ else:
+ pass # leave it alone to avoid breaking conditional
+ else:
+ if i > 0 and words[i-1].rstrip('.,،؛;:!؟?()[]{}«»"\'…') in ('قال', 'قالت', 'يقول', 'يقولون', 'قلت', 'قلنا', 'تقول', 'يقولوا'):
+ words[i] = _INNA_SENTENCE_INITIAL[clean_w] + w[len(clean_w):]
+ else:
+ words[i] = _ANNA_MID_SENTENCE[clean_w] + w[len(clean_w):]
continue
for stem, fixed in _HAMZA_STEMS.items():
if w.startswith(stem) and len(w) > len(stem):