youssefreda9 commited on
Commit
df5e501
·
1 Parent(s): 5ed1cab

Fix collision benchmark failures: patch overlap mapping, tokenization guard, and rule hallucinations

Browse files
src/app.py CHANGED
@@ -693,7 +693,7 @@ class OffsetMapper:
693
  for tag, i1, i2, j1, j2 in s.get_opcodes():
694
  self._opcodes.append((i1, i2, j1, j2))
695
 
696
- def reverse_map_offset(self, pos_in_after):
697
  """
698
  Map a single position from text_after → text_before.
699
  (CURRENT_TEXT after mutation → CURRENT_TEXT before mutation)
@@ -701,13 +701,27 @@ class OffsetMapper:
701
  Used by PipelineContext.map_to_original() to walk the mapper
702
  chain in reverse, ultimately reaching ORIGINAL_TEXT coordinates.
703
  """
 
704
  for i1, i2, j1, j2 in self._opcodes:
705
  if j1 <= pos_in_after <= j2:
706
- if j2 == j1: # insertion point
707
- return i1
 
 
 
 
 
 
 
 
 
 
708
  ratio = (pos_in_after - j1) / (j2 - j1)
709
- return round(i1 + ratio * (i2 - i1)) # FIX-12: round() instead of int() truncation
710
- return len(self._text_before)
 
 
 
711
 
712
  def forward_map_range(self, start_in_before, end_in_before):
713
  """
 
693
  for tag, i1, i2, j1, j2 in s.get_opcodes():
694
  self._opcodes.append((i1, i2, j1, j2))
695
 
696
+ def reverse_map_offset(self, pos_in_after, is_end=False):
697
  """
698
  Map a single position from text_after → text_before.
699
  (CURRENT_TEXT after mutation → CURRENT_TEXT before mutation)
 
701
  Used by PipelineContext.map_to_original() to walk the mapper
702
  chain in reverse, ultimately reaching ORIGINAL_TEXT coordinates.
703
  """
704
+ matches = []
705
  for i1, i2, j1, j2 in self._opcodes:
706
  if j1 <= pos_in_after <= j2:
707
+ matches.append((i1, i2, j1, j2))
708
+
709
+ if not matches:
710
+ return len(self._text_before)
711
+
712
+ mapped_positions = []
713
+ for i1, i2, j1, j2 in matches:
714
+ if j2 == j1: # insertion point in text_before (deleted in text_after)
715
+ # If we're mapping an 'end' coordinate, we want to encompass the deleted text (i2).
716
+ # If we're mapping a 'start' coordinate, we want the start of the deletion (i1).
717
+ mapped_positions.append(i2 if is_end else i1)
718
+ else:
719
  ratio = (pos_in_after - j1) / (j2 - j1)
720
+ mapped_positions.append(round(i1 + ratio * (i2 - i1)))
721
+
722
+ # If is_end is True, maximize the mapped offset (include as much as possible)
723
+ # If is_end is False, minimize the mapped offset
724
+ return max(mapped_positions) if is_end else min(mapped_positions)
725
 
726
  def forward_map_range(self, start_in_before, end_in_before):
727
  """
src/nlp/grammar/grammar_rules.py CHANGED
@@ -765,16 +765,38 @@ class ArabicGrammarGuard:
765
  ('fix_conditional_sentences', self.fix_conditional_sentences),
766
  ('fix_tanween_fathah', self.fix_tanween_fathah),
767
  ('fix_initial_hamza', self.fix_initial_hamza),
 
768
  ('regex_rules_fallback', self.regex_rules_fallback),
769
  ]:
770
  try:
771
- text = rule_fn(text)
772
  except Exception as e:
773
  logger.warning(f"[GRAMMAR-RULES] {rule_name} failed: {e}")
774
 
775
  text = re.sub(r'\s+', ' ', text).strip()
776
  return text
777
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
778
  def fix_tanween_fathah(self, text):
779
  """
780
  Add tanween fathah (ً) to indefinite accusative nouns ending in ا.
 
765
  ('fix_conditional_sentences', self.fix_conditional_sentences),
766
  ('fix_tanween_fathah', self.fix_tanween_fathah),
767
  ('fix_initial_hamza', self.fix_initial_hamza),
768
+ ('fix_suffix_hallucination', self.fix_suffix_hallucination),
769
  ('regex_rules_fallback', self.regex_rules_fallback),
770
  ]:
771
  try:
772
+ text = rule_fn(text, original_text) if rule_name == 'fix_suffix_hallucination' else rule_fn(text)
773
  except Exception as e:
774
  logger.warning(f"[GRAMMAR-RULES] {rule_name} failed: {e}")
775
 
776
  text = re.sub(r'\s+', ' ', text).strip()
777
  return text
778
 
779
+ def fix_suffix_hallucination(self, text, original_text):
780
+ """
781
+ Revert grammar hallucination where extra consonants are appended to pronoun suffixes.
782
+ Example: شجعتهم → شجعتهمت
783
+ """
784
+ orig_words = original_text.split()
785
+ curr_words = text.split()
786
+
787
+ if len(orig_words) == len(curr_words):
788
+ for i in range(len(orig_words)):
789
+ ow = orig_words[i]
790
+ cw = curr_words[i]
791
+ # If the current word is just the original word + 1 consonant, and original ended in a suffix
792
+ if len(cw) == len(ow) + 1 and cw.startswith(ow):
793
+ added_char = cw[-1]
794
+ if ow.endswith(('هم', 'هن', 'كم', 'كن', 'ها', 'نا')) and added_char in 'تمةنل':
795
+ curr_words[i] = ow
796
+ logger.info(f"[GRAMMAR-RULES] Reverted suffix hallucination: {cw} → {ow}")
797
+ text = ' '.join(curr_words)
798
+ return text
799
+
800
  def fix_tanween_fathah(self, text):
801
  """
802
  Add tanween fathah (ً) to indefinite accusative nouns ending in ا.
src/nlp/pipeline_context.py CHANGED
@@ -48,8 +48,8 @@ class PipelineContext:
48
  """
49
  curr_start, curr_end = start, end
50
  for mapper in reversed(self._offset_mappers):
51
- curr_start = mapper.reverse_map_offset(curr_start)
52
- curr_end = mapper.reverse_map_offset(curr_end)
53
  return curr_start, curr_end
54
 
55
  def add_patch(self, stage: str, start_current: int, end_current: int,
 
48
  """
49
  curr_start, curr_end = start, end
50
  for mapper in reversed(self._offset_mappers):
51
+ curr_start = mapper.reverse_map_offset(curr_start, is_end=False)
52
+ curr_end = mapper.reverse_map_offset(curr_end, is_end=True)
53
  return curr_start, curr_end
54
 
55
  def add_patch(self, stage: str, start_current: int, end_current: int,
src/nlp/punctuation/punctuation_rules.py CHANGED
@@ -67,9 +67,9 @@ def arabic_postprocessing(text: str) -> str:
67
  if re.fullmatch(_ALLOWED_COLON_CUES, prev_word):
68
  return match.group(0)
69
  # If it's a definite noun (starts with ال) and not in allowed list, it's hallucinated.
70
- # e.g., "الشمس:" -> "الشمس،"
71
  if prev_word.startswith('ال'):
72
- return f'{prev_word}،'
73
  return match.group(0)
74
 
75
  text = re.sub(r'([\u0600-\u06FF]+)(\s*:)', _colon_guard, text)
 
67
  if re.fullmatch(_ALLOWED_COLON_CUES, prev_word):
68
  return match.group(0)
69
  # If it's a definite noun (starts with ال) and not in allowed list, it's hallucinated.
70
+ # Remove the colon entirely — replacing with comma is also wrong (e.g. الشمس، مشرقة)
71
  if prev_word.startswith('ال'):
72
+ return f'{prev_word}'
73
  return match.group(0)
74
 
75
  text = re.sub(r'([\u0600-\u06FF]+)(\s*:)', _colon_guard, text)
src/nlp/spelling/araspell_rules.py CHANGED
@@ -247,10 +247,13 @@ class AraSpellPostProcessor:
247
  'اقرا': 'أقرأ', 'اقرأ': 'أقرأ',
248
  'اعمل': 'أعمل', 'ادرس': 'أدرس',
249
  'اشتري': 'أشتري', 'اسافر': 'أسافر',
 
250
  'مسؤول': 'مسؤول', 'مسؤولية': 'مسؤولية',
251
  'رؤية': 'رؤية', 'رؤيا': 'رؤيا',
252
  'مؤسسة': 'مؤسسة', 'مؤتمر': 'مؤتمر',
253
  'تأثير': 'تأثير', 'تأكيد': 'تأكيد',
 
 
254
  # FIX-14: Alif maqsura common errors
255
  'المستشفي': 'المستشفى',
256
  'مصطفي': 'مصطفى', 'موسي': 'موسى', 'عيسي': 'عيسى',
@@ -1618,14 +1621,42 @@ class ArabicSpellChecker:
1618
  result = AraSpellPostProcessor.fix_ha_ta_marbuta(result, vocab_manager=self.vocab_manager)
1619
 
1620
  # 11. DESTRUCTIVE TOKENIZATION GUARD
1621
- # Arabic orthography does not use standalone 1-letter words.
1622
- # If the model creates a standalone 1-letter word that was not in the original, it's a tokenization hallucination.
 
1623
  orig_standalone = set(w for w in original.split() if len(w) == 1)
 
1624
  res_words_list = result.split()
1625
- for w in res_words_list:
1626
  if len(w) == 1 and w not in orig_standalone:
1627
  if w in 'واتيبلفك':
1628
- logger.info(f"[SPELLING] Blocked destructive tokenization (hallucinated standalone '{w}'): '{original}' -> '{result}'")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1629
  result = original
1630
  break
1631
 
 
247
  'اقرا': 'أقرأ', 'اقرأ': 'أقرأ',
248
  'اعمل': 'أعمل', 'ادرس': 'أدرس',
249
  'اشتري': 'أشتري', 'اسافر': 'أسافر',
250
+ 'احبه': 'أحبه',
251
  'مسؤول': 'مسؤول', 'مسؤولية': 'مسؤولية',
252
  'رؤية': 'رؤية', 'رؤيا': 'رؤيا',
253
  'مؤسسة': 'مؤسسة', 'مؤتمر': 'مؤتمر',
254
  'تأثير': 'تأثير', 'تأكيد': 'تأكيد',
255
+ 'البنايه': 'البناية',
256
+ 'جدا': 'جداً', 'جداً': 'جداً',
257
  # FIX-14: Alif maqsura common errors
258
  'المستشفي': 'المستشفى',
259
  'مصطفي': 'مصطفى', 'موسي': 'موسى', 'عيسي': 'عيسى',
 
1621
  result = AraSpellPostProcessor.fix_ha_ta_marbuta(result, vocab_manager=self.vocab_manager)
1622
 
1623
  # 11. DESTRUCTIVE TOKENIZATION GUARD
1624
+ # Arabic orthography does not use standalone 1-letter words except prepositions.
1625
+ # If the model creates a standalone 1-letter word that was not in the original,
1626
+ # check if it's a legitimate prefix separation (e.g. بالشاروع→ب الشارع).
1627
  orig_standalone = set(w for w in original.split() if len(w) == 1)
1628
+ orig_words = original.split()
1629
  res_words_list = result.split()
1630
+ for idx, w in enumerate(res_words_list):
1631
  if len(w) == 1 and w not in orig_standalone:
1632
  if w in 'واتيبلفك':
1633
+ # Check if this is a legitimate prefix separation:
1634
+ # The original word should have started with this letter as a prefix
1635
+ is_prefix_separation = False
1636
+ if w in 'وفبلك' and idx + 1 < len(res_words_list):
1637
+ next_word = res_words_list[idx + 1]
1638
+ combined = w + next_word
1639
+ # If any original word started with the prefix letter and
1640
+ # the remainder matches the next word, it's legitimate
1641
+ for ow in orig_words:
1642
+ if ow.startswith(w) and len(ow) > 2:
1643
+ is_prefix_separation = True
1644
+ break
1645
+
1646
+ if not is_prefix_separation:
1647
+ logger.info(f"[SPELLING] Blocked destructive tokenization (hallucinated standalone '{w}'): '{original}' -> '{result}'")
1648
+ result = original
1649
+ break
1650
+
1651
+ # 12. MORPHOLOGICAL MUTATION GUARD (Verb -> Noun)
1652
+ # Prevents spelling from changing a plural verb (e.g. صممو) to a noun (e.g. مصممو) by prepending م
1653
+ if len(orig_words) == len(res_words_list):
1654
+ for idx in range(len(orig_words)):
1655
+ ow = orig_words[idx]
1656
+ rw = res_words_list[idx]
1657
+ # If the word didn't start with م but the correction does, and it looks like a plural verb
1658
+ if not ow.startswith('م') and rw.startswith('م') and rw[1:] == ow and ow.endswith('و'):
1659
+ logger.info(f"[SPELLING] Blocked morphological mutation (verb→noun '{ow}'→'{rw}'): '{original}' -> '{result}'")
1660
  result = original
1661
  break
1662