Bayan AI commited on
Commit ·
19a801d
1
Parent(s): 44c30d6
Feat: Add demonstrative and noun-adjective dual/plural agreement rules
Browse files- src/nlp/grammar/grammar_rules.py +99 -0
- test_grammar_only.py +2 -1
- test_pos.py +8 -0
src/nlp/grammar/grammar_rules.py
CHANGED
|
@@ -407,6 +407,103 @@ class ArabicGrammarGuard:
|
|
| 407 |
|
| 408 |
return " ".join(corrected_tokens)
|
| 409 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
|
| 411 |
def process(self, original_text, generated_text):
|
| 412 |
"""Apply all grammar rules to model output."""
|
|
@@ -415,10 +512,12 @@ class ArabicGrammarGuard:
|
|
| 415 |
# Each rule is wrapped in try/except so that if camel-tools
|
| 416 |
# functions fail, the regex-based rules still execute.
|
| 417 |
for rule_name, rule_fn in [
|
|
|
|
| 418 |
('fix_number_and_gender_agreement', self.fix_number_and_gender_agreement),
|
| 419 |
('smart_asmaa_khamsa_fix', self.smart_asmaa_khamsa_fix),
|
| 420 |
('fix_verbs_nasb_and_jazm', self.fix_verbs_nasb_and_jazm),
|
| 421 |
('fix_gender_agreement', self.fix_gender_agreement),
|
|
|
|
| 422 |
('fix_prepositions_advanced', self.fix_prepositions_advanced),
|
| 423 |
('fix_subject_verb_agreement', self.fix_subject_verb_agreement),
|
| 424 |
('fix_conditional_sentences', self.fix_conditional_sentences),
|
|
|
|
| 407 |
|
| 408 |
return " ".join(corrected_tokens)
|
| 409 |
|
| 410 |
+
def fix_demonstrative_agreement(self, text):
|
| 411 |
+
tokens = simple_word_tokenize(text)
|
| 412 |
+
disambig_tokens = self.mle.disambiguate(tokens)
|
| 413 |
+
corrected_tokens = list(tokens)
|
| 414 |
+
|
| 415 |
+
for i in range(len(disambig_tokens) - 1):
|
| 416 |
+
w1 = corrected_tokens[i]
|
| 417 |
+
w2 = corrected_tokens[i+1]
|
| 418 |
+
|
| 419 |
+
if w1 not in ['هذا', 'هذه', 'هذان', 'هاتان', 'هذين', 'هاتين', 'هؤلاء']:
|
| 420 |
+
continue
|
| 421 |
+
|
| 422 |
+
w2_info = disambig_tokens[i+1].analyses[0].analysis if disambig_tokens[i+1].analyses else {}
|
| 423 |
+
w2_num = w2_info.get('num', 's')
|
| 424 |
+
w2_gen = w2_info.get('gen', 'm')
|
| 425 |
+
|
| 426 |
+
# Use heuristics to override or reinforce POS tags for duals
|
| 427 |
+
if w2.endswith('تان') or w2.endswith('تين'):
|
| 428 |
+
if w2_num == 'd':
|
| 429 |
+
w2_gen = 'f'
|
| 430 |
+
|
| 431 |
+
if w2_num == 'd':
|
| 432 |
+
is_nom = w2.endswith('ان') or w2.endswith('تان')
|
| 433 |
+
if w2_gen == 'f':
|
| 434 |
+
corrected_tokens[i] = 'هاتان' if is_nom else 'هاتين'
|
| 435 |
+
elif w2_gen == 'm':
|
| 436 |
+
corrected_tokens[i] = 'هذان' if is_nom else 'هذين'
|
| 437 |
+
|
| 438 |
+
return " ".join(corrected_tokens)
|
| 439 |
+
|
| 440 |
+
def fix_noun_adjective_agreement_advanced(self, text):
|
| 441 |
+
tokens = simple_word_tokenize(text)
|
| 442 |
+
disambig_tokens = self.mle.disambiguate(tokens)
|
| 443 |
+
corrected_tokens = list(tokens)
|
| 444 |
+
|
| 445 |
+
MASC_TO_FEM_ADJ = {
|
| 446 |
+
'جميل': 'جميلة', 'كبير': 'كبيرة', 'صغير': 'صغيرة',
|
| 447 |
+
'طويل': 'طويلة', 'قصير': 'قصيرة', 'جديد': 'جديدة',
|
| 448 |
+
'قديم': 'قديمة', 'بعيد': 'بعيدة', 'قريب': 'قريبة',
|
| 449 |
+
'سريع': 'سريعة', 'بطيء': 'بطيئة', 'واسع': 'واسعة',
|
| 450 |
+
'ضيق': 'ضيقة', 'عميق': 'عميقة', 'خفيف': 'خفيفة',
|
| 451 |
+
'ثقيل': 'ثقيلة', 'نظيف': 'نظيفة', 'مشرق': 'مشرقة',
|
| 452 |
+
'ذكي': 'ذكية', 'غني': 'غنية', 'فقير': 'فقيرة',
|
| 453 |
+
'متفوق': 'متفوقة', 'مجتهد': 'مجتهدة', 'ممتاز': 'ممتازة',
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
for i in range(len(disambig_tokens) - 1):
|
| 457 |
+
w1 = corrected_tokens[i]
|
| 458 |
+
w2 = corrected_tokens[i+1]
|
| 459 |
+
|
| 460 |
+
w1_info = disambig_tokens[i].analyses[0].analysis if disambig_tokens[i].analyses else {}
|
| 461 |
+
w1_pos = w1_info.get('pos', 'unknown')
|
| 462 |
+
w1_num = w1_info.get('num', 's')
|
| 463 |
+
w1_gen = w1_info.get('gen', 'm')
|
| 464 |
+
|
| 465 |
+
# Heuristic override for duals (camel_tools sometimes gets them wrong)
|
| 466 |
+
if w1.endswith('تان') or w1.endswith('تين'):
|
| 467 |
+
w1_gen = 'f'
|
| 468 |
+
w1_num = 'd'
|
| 469 |
+
elif w1.endswith('ان') or w1.endswith('ين'):
|
| 470 |
+
if len(w1) > 4:
|
| 471 |
+
w1_num = 'd'
|
| 472 |
+
|
| 473 |
+
# Dual Adjective Agreement
|
| 474 |
+
if w1_num == 'd' and w1_pos in ['noun', 'unknown', 'noun_prop']:
|
| 475 |
+
base_adj = None
|
| 476 |
+
for suffix in ['ان', 'ين', 'تان', 'تين', 'ة', 'ون', 'ات', '']:
|
| 477 |
+
stem = w2[:-len(suffix)] if suffix else w2
|
| 478 |
+
if stem in MASC_TO_FEM_ADJ:
|
| 479 |
+
base_adj = stem
|
| 480 |
+
break
|
| 481 |
+
|
| 482 |
+
if base_adj:
|
| 483 |
+
is_nom = w1.endswith('ان') or w1.endswith('تان')
|
| 484 |
+
if w1_gen == 'f':
|
| 485 |
+
corrected_tokens[i+1] = base_adj + ('تان' if is_nom else 'تين')
|
| 486 |
+
else:
|
| 487 |
+
corrected_tokens[i+1] = base_adj + ('ان' if is_nom else 'ين')
|
| 488 |
+
|
| 489 |
+
# Plural Human Adjective Agreement
|
| 490 |
+
elif w1_num == 'p' and w1_pos in ['noun', 'unknown']:
|
| 491 |
+
base_adj = None
|
| 492 |
+
for suffix in ['ان', 'ين', 'تان', 'تين', 'ة', 'ون', 'ات', 'ين', '']:
|
| 493 |
+
stem = w2[:-len(suffix)] if suffix else w2
|
| 494 |
+
if stem in MASC_TO_FEM_ADJ:
|
| 495 |
+
base_adj = stem
|
| 496 |
+
break
|
| 497 |
+
|
| 498 |
+
if base_adj:
|
| 499 |
+
if w1.endswith('ون') or w1.endswith('ين') or w1_gen == 'm':
|
| 500 |
+
is_nom = w1.endswith('ون')
|
| 501 |
+
corrected_tokens[i+1] = base_adj + ('ون' if is_nom else 'ين')
|
| 502 |
+
elif w1.endswith('ات') or w1_gen == 'f':
|
| 503 |
+
corrected_tokens[i+1] = base_adj + 'ات'
|
| 504 |
+
|
| 505 |
+
return " ".join(corrected_tokens)
|
| 506 |
+
|
| 507 |
|
| 508 |
def process(self, original_text, generated_text):
|
| 509 |
"""Apply all grammar rules to model output."""
|
|
|
|
| 512 |
# Each rule is wrapped in try/except so that if camel-tools
|
| 513 |
# functions fail, the regex-based rules still execute.
|
| 514 |
for rule_name, rule_fn in [
|
| 515 |
+
('fix_demonstrative_agreement', self.fix_demonstrative_agreement),
|
| 516 |
('fix_number_and_gender_agreement', self.fix_number_and_gender_agreement),
|
| 517 |
('smart_asmaa_khamsa_fix', self.smart_asmaa_khamsa_fix),
|
| 518 |
('fix_verbs_nasb_and_jazm', self.fix_verbs_nasb_and_jazm),
|
| 519 |
('fix_gender_agreement', self.fix_gender_agreement),
|
| 520 |
+
('fix_noun_adjective_agreement_advanced', self.fix_noun_adjective_agreement_advanced),
|
| 521 |
('fix_prepositions_advanced', self.fix_prepositions_advanced),
|
| 522 |
('fix_subject_verb_agreement', self.fix_subject_verb_agreement),
|
| 523 |
('fix_conditional_sentences', self.fix_conditional_sentences),
|
test_grammar_only.py
CHANGED
|
@@ -18,7 +18,8 @@ def test_grammar():
|
|
| 18 |
"يذهبون المهندسون الى الشركة",
|
| 19 |
"يذهبون المهندسون الى الشركة، أليس كذلك؟",
|
| 20 |
"مرحبا، يا عالم الطيور!",
|
| 21 |
-
"متى يسعون في تطوير مهاراتكم بجد واخلاص تجدون ثمرة ذلك قريبا"
|
|
|
|
| 22 |
]
|
| 23 |
|
| 24 |
for text in test_cases:
|
|
|
|
| 18 |
"يذهبون المهندسون الى الشركة",
|
| 19 |
"يذهبون المهندسون الى الشركة، أليس كذلك؟",
|
| 20 |
"مرحبا، يا عالم الطيور!",
|
| 21 |
+
"متى يسعون في تطوير مهاراتكم بجد واخلاص تجدون ثمرة ذلك قريبا",
|
| 22 |
+
"هذان فتاتان جميل."
|
| 23 |
]
|
| 24 |
|
| 25 |
for text in test_cases:
|
test_pos.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 3 |
+
from camel_tools.tokenizers.word import simple_word_tokenize
|
| 4 |
+
from camel_tools.disambig.mle import MLEDisambiguator
|
| 5 |
+
|
| 6 |
+
mle = MLEDisambiguator.pretrained()
|
| 7 |
+
t = simple_word_tokenize('فتاتان جميل')
|
| 8 |
+
print([a.analyses[0].analysis for a in mle.disambiguate(t)])
|