Bayan AI commited on
Commit
8f8ed51
·
1 Parent(s): e661f43

Fix collision resolution and punctuation patch application errors

Browse files
Files changed (2) hide show
  1. src/app.py +63 -8
  2. src/nlp/correction_patch.py +30 -27
src/app.py CHANGED
@@ -55,6 +55,12 @@ from model_loader import (
55
  PUNCTUATION_PATH
56
  )
57
 
 
 
 
 
 
 
58
  # HuggingFace Inference API — used in production to avoid RAM limits
59
  from hf_inference import (
60
  hf_summarize,
@@ -1756,8 +1762,24 @@ def analyze_text():
1756
  logger.info(f"[SPELLING] Rejected: '{o_word}'→'{c_word}' (filter blocked)")
1757
  new_words.append(current_text[start_idx:end_idx])
1758
  elif len(o_segment) == 1 and len(c_segment) > 1:
1759
- # 1-word → N words: accept word splits (e.g. فيالمدرسة → في المدرسة)
1760
  o_word = o_segment[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1761
  if len(o_word) >= 5 and ' ' not in o_word:
1762
  corr_str = " ".join(c_segment)
1763
  # ── Phase 3 (BUG-021/028/029): validate split parts ──
@@ -1773,7 +1795,7 @@ def analyze_text():
1773
  'هم', 'هن', 'ها', 'هما', 'كم', 'كن', 'نا',
1774
  'ه', 'ك', # single-char pronouns
1775
  }
1776
- if _parts_ok and len(c_segment) == 2:
1777
  last_part = c_segment[-1]
1778
  if last_part in _ATTACHED_PRONOUNS:
1779
  # Check if joined form ≈ original (pronoun was attached)
@@ -1784,6 +1806,15 @@ def analyze_text():
1784
  f"[SPELLING] Rejected split: '{o_word}'→'{corr_str}' "
1785
  f"(detached pronoun suffix '{last_part}')"
1786
  )
 
 
 
 
 
 
 
 
 
1787
  if _parts_ok:
1788
  new_words.append(corr_str)
1789
  ctx.add_patch(
@@ -1846,6 +1877,16 @@ def analyze_text():
1846
  'هم', 'هن', 'ها', 'هما', 'كم', 'كن', 'نا',
1847
  'ه', 'ك',
1848
  }
 
 
 
 
 
 
 
 
 
 
1849
  if _parts_ok and len(split_parts) == 2:
1850
  if split_parts[-1] in _ATTACHED_PRON:
1851
  if _levenshtein(o_word, joined_no_space) <= 2:
@@ -2248,6 +2289,12 @@ def analyze_text():
2248
  # Gender with ي: ذكي→ذكية
2249
  elif (_c_cl.endswith('ية') and _c_cl[:-1] == _o_cl[:-1] + 'ي' and _o_cl.endswith('ي') and len(_o_cl) >= 3):
2250
  _is_grammar_pattern = True
 
 
 
 
 
 
2251
 
2252
  # StageLocker: skip diffs that overlap with locked ranges
2253
  # Phase 11: Hierarchy-aware — grammar (3) overrides spelling (2)
@@ -2275,6 +2322,14 @@ def analyze_text():
2275
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"Jaccard_03","original":orig_text[:80],"correction":corr_text[:80],"jaccard":round(jaccard,3)})}')
2276
  _tel_events.append({"event":"filter_reject","filter":"Jaccard_03","original":orig_text[:80],"correction":corr_text[:80],"jaccard":round(jaccard,3)})
2277
  continue
 
 
 
 
 
 
 
 
2278
 
2279
  # ── FIX-13: Named entity protection ──
2280
  # Reject grammar changes to words that look like proper nouns:
@@ -2572,13 +2627,13 @@ def analyze_text():
2572
  f"{len(punc_patches)} → {MAX_PUNC_PATCHES_PER_RESPONSE}"
2573
  )
2574
 
2575
- # FIX-05: Rebuild punctuation text from accepted diffs only
2576
  _safe_punc = ctx.current_text
2577
- _punc_accepted = [d for d in diffs if validate_punctuation_diff(d, full_text=ctx.current_text)]
2578
- for _pd in sorted(_punc_accepted, key=lambda x: x['start'], reverse=True):
2579
- _safe_punc = (_safe_punc[:_pd['start']] +
2580
- _pd['correction'] +
2581
- _safe_punc[_pd['end']:])
2582
  ctx.mutate_text(_safe_punc, OffsetMapper)
2583
  current_text = ctx.current_text
2584
 
 
55
  PUNCTUATION_PATH
56
  )
57
 
58
+ # Optional Pipeline Stages
59
+ ENABLE_DIALECT_MODEL = False
60
+ ENABLE_PUNCTUATION_MODEL = True
61
+ ENABLE_GRAMMAR_MODEL = True
62
+ ENABLE_AUTOCOMPLETE_MODEL = False
63
+
64
  # HuggingFace Inference API — used in production to avoid RAM limits
65
  from hf_inference import (
66
  hf_summarize,
 
1762
  logger.info(f"[SPELLING] Rejected: '{o_word}'→'{c_word}' (filter blocked)")
1763
  new_words.append(current_text[start_idx:end_idx])
1764
  elif len(o_segment) == 1 and len(c_segment) > 1:
 
1765
  o_word = o_segment[0]
1766
+
1767
+ # FIX-021: AraSpell seq2seq sometimes detaches prefixes (e.g. بالشاروع -> ب الشارع)
1768
+ # Re-attach known prefixes before processing the split
1769
+ if len(c_segment) == 2 and c_segment[0] in {'ب', 'ف', 'ك', 'ل'}:
1770
+ rejoined_word = "".join(c_segment)
1771
+ _spell_conf = _is_small_spelling_change(o_word, rejoined_word, spell_checker.vocab_manager)
1772
+ if _spell_conf:
1773
+ logger.info(f"[SPELLING] Re-attached prefix: '{o_word}'→'{rejoined_word}'")
1774
+ new_words.append(rejoined_word)
1775
+ ctx.add_patch(
1776
+ 'spelling', start_idx, end_idx,
1777
+ rejoined_word, confidence=_spell_conf,
1778
+ alternatives=[rejoined_word, o_word],
1779
+ )
1780
+ continue
1781
+
1782
+ # 1-word → N words: accept word splits (e.g. فيالمدرسة → في المدرسة)
1783
  if len(o_word) >= 5 and ' ' not in o_word:
1784
  corr_str = " ".join(c_segment)
1785
  # ── Phase 3 (BUG-021/028/029): validate split parts ──
 
1795
  'هم', 'هن', 'ها', 'هما', 'كم', 'كن', 'نا',
1796
  'ه', 'ك', # single-char pronouns
1797
  }
1798
+ if _parts_ok and len(c_segment) >= 2:
1799
  last_part = c_segment[-1]
1800
  if last_part in _ATTACHED_PRONOUNS:
1801
  # Check if joined form ≈ original (pronoun was attached)
 
1806
  f"[SPELLING] Rejected split: '{o_word}'→'{corr_str}' "
1807
  f"(detached pronoun suffix '{last_part}')"
1808
  )
1809
+ elif len(c_segment) >= 2:
1810
+ for idx, p in enumerate(c_segment):
1811
+ if p in {'و', 'ف', 'ب', 'ل', 'ك'} and idx > 0:
1812
+ _parts_ok = False
1813
+ logger.info(
1814
+ f"[SPELLING] Rejected split: '{o_word}'→'{corr_str}' "
1815
+ f"(invalid standalone conjunction '{p}' inside split)"
1816
+ )
1817
+ break
1818
  if _parts_ok:
1819
  new_words.append(corr_str)
1820
  ctx.add_patch(
 
1877
  'هم', 'هن', 'ها', 'هما', 'كم', 'كن', 'نا',
1878
  'ه', 'ك',
1879
  }
1880
+ if _parts_ok and len(split_parts) >= 2:
1881
+ for idx, p in enumerate(split_parts):
1882
+ if p in {'و', 'ف', 'ب', 'ل', 'ك'} and idx > 0:
1883
+ _parts_ok = False
1884
+ logger.info(
1885
+ f"[SPELLING] Rejected N→M split: '{o_word}'→'{corr_str}' "
1886
+ f"(invalid standalone conjunction '{p}' inside split)"
1887
+ )
1888
+ break
1889
+
1890
  if _parts_ok and len(split_parts) == 2:
1891
  if split_parts[-1] in _ATTACHED_PRON:
1892
  if _levenshtein(o_word, joined_no_space) <= 2:
 
2289
  # Gender with ي: ذكي→ذكية
2290
  elif (_c_cl.endswith('ية') and _c_cl[:-1] == _o_cl[:-1] + 'ي' and _o_cl.endswith('ي') and len(_o_cl) >= 3):
2291
  _is_grammar_pattern = True
2292
+ # Pronoun gender agreement: ه/ة → ها
2293
+ elif (_c_cl.endswith('ها') and _o_cl[-1:] in ('ه', 'ة') and _c_cl[:-2] == _o_cl[:-1] and len(_o_cl) >= 3):
2294
+ _is_grammar_pattern = True
2295
+ # Pronoun gender agreement: ها → ه
2296
+ elif (_c_cl.endswith('ه') and _o_cl.endswith('ها') and _c_cl[:-1] == _o_cl[:-2] and len(_o_cl) >= 4):
2297
+ _is_grammar_pattern = True
2298
 
2299
  # StageLocker: skip diffs that overlap with locked ranges
2300
  # Phase 11: Hierarchy-aware — grammar (3) overrides spelling (2)
 
2322
  logger.info(f'[FILTER-TEL] {_tel_json.dumps({"event":"filter_reject","filter":"Jaccard_03","original":orig_text[:80],"correction":corr_text[:80],"jaccard":round(jaccard,3)})}')
2323
  _tel_events.append({"event":"filter_reject","filter":"Jaccard_03","original":orig_text[:80],"correction":corr_text[:80],"jaccard":round(jaccard,3)})
2324
  continue
2325
+
2326
+ # Reject dialect/hallucination verb suffixes like "تون"
2327
+ if orig_text and corr_text:
2328
+ _c_clean = corr_text.rstrip('.,،؛;:!؟?()[]{}«»"\'…')
2329
+ _o_clean = orig_text.rstrip('.,،؛;:!؟?()[]{}«»"\'…')
2330
+ if _c_clean.endswith('تون') and not _o_clean.endswith('تون'):
2331
+ logger.info(f"[GRAMMAR] Rejected hallucination: '{orig_text}'→'{corr_text}' (invalid suffix 'تون')")
2332
+ continue
2333
 
2334
  # ── FIX-13: Named entity protection ──
2335
  # Reject grammar changes to words that look like proper nouns:
 
2627
  f"{len(punc_patches)} → {MAX_PUNC_PATCHES_PER_RESPONSE}"
2628
  )
2629
 
2630
+ # FIX-05: Rebuild punctuation text from accepted diffs only (respecting locks and caps)
2631
  _safe_punc = ctx.current_text
2632
+ _final_punc_patches = [p for p in ctx.patches.patches if p.stage == 'punctuation']
2633
+ for _p in sorted(_final_punc_patches, key=lambda x: x.start_current, reverse=True):
2634
+ _safe_punc = (_safe_punc[:_p.start_current] +
2635
+ _p.replacement +
2636
+ _safe_punc[_p.end_current:])
2637
  ctx.mutate_text(_safe_punc, OffsetMapper)
2638
  current_text = ctx.current_text
2639
 
src/nlp/correction_patch.py CHANGED
@@ -121,36 +121,39 @@ class PatchSet:
121
  for ci, (cs, ce, claimed_stage, res_idx) in enumerate(claimed_ranges):
122
  # Check if there's any overlap at all
123
  if patch.start_original < ce and patch.end_original > cs:
124
- # ── Phase 14: Cross-stage compatibility ──
125
- # Spelling + Punctuation are COMPATIBLE stages:
126
- # spelling fixes the word, punctuation adds marks.
127
- # They should never conflict.
128
- _compatible_pair = {
129
- frozenset({'spelling', 'punctuation'}),
130
- }
131
- if frozenset({patch.stage, claimed_stage}) in _compatible_pair:
132
- continue # Compatible stages — allow coexistence
133
-
134
- # ── FIX-36: Grammar + Punctuation merge ──
135
- # When punctuation adds a trailing character to a grammar
136
- # correction at the same span, merge instead of dropping.
137
- if (patch.stage == 'punctuation' and claimed_stage == 'grammar'
138
- and patch.start_original == cs and patch.end_original == ce):
139
- # Check if punctuation correction = grammar correction + punct char
140
- grammar_patch = resolved[res_idx]
141
  punc_correction = patch.replacement
142
- gram_correction = grammar_patch.replacement
143
- if (len(punc_correction) == len(gram_correction) + 1
144
- and punc_correction.startswith(gram_correction)
145
- and punc_correction[-1] in _PUNCT_CHARS):
146
- # Merge: append the trailing punct to grammar correction
147
- grammar_patch.replacement = punc_correction
 
148
  logger.info(
149
- f"[OVERLAP] Merged punctuation into grammar "
150
- f"[{cs}:{ce}]: '{grammar_patch.original}' → "
151
- f"'{grammar_patch.replacement}'"
152
  )
153
- has_substantial_overlap = True # Don't add separately
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  break
155
 
156
  # Calculate overlap amount
 
121
  for ci, (cs, ce, claimed_stage, res_idx) in enumerate(claimed_ranges):
122
  # Check if there's any overlap at all
123
  if patch.start_original < ce and patch.end_original > cs:
124
+ # ── FIX-36 & Phase 14: Generalized Punctuation Merge ──
125
+ # If punctuation adds characters to a grammar or spelling correction,
126
+ # merge them instead of coexisting. Coexisting overlapping patches
127
+ # break _apply_patches_to_original.
128
+ if patch.stage == 'punctuation' and claimed_stage in ('grammar', 'spelling'):
129
+ claimed_patch = resolved[res_idx]
 
 
 
 
 
 
 
 
 
 
 
130
  punc_correction = patch.replacement
131
+ prev_correction = claimed_patch.replacement
132
+
133
+ # Check if punctuation is just appending trailing punctuation
134
+ if (len(punc_correction) > len(prev_correction)
135
+ and punc_correction.startswith(prev_correction)
136
+ and all(c in _PUNCT_CHARS for c in punc_correction[len(prev_correction):])):
137
+ claimed_patch.replacement = punc_correction
138
  logger.info(
139
+ f"[OVERLAP] Merged trailing punctuation into {claimed_stage} "
140
+ f"[{cs}:{ce}]: '{claimed_patch.original}' → "
141
+ f"'{claimed_patch.replacement}'"
142
  )
143
+ has_substantial_overlap = True
144
+ break
145
+
146
+ # Check if punctuation is just prepending leading punctuation
147
+ if (len(punc_correction) > len(prev_correction)
148
+ and punc_correction.endswith(prev_correction)
149
+ and all(c in _PUNCT_CHARS for c in punc_correction[:-len(prev_correction)])):
150
+ claimed_patch.replacement = punc_correction
151
+ logger.info(
152
+ f"[OVERLAP] Merged leading punctuation into {claimed_stage} "
153
+ f"[{cs}:{ce}]: '{claimed_patch.original}' → "
154
+ f"'{claimed_patch.replacement}'"
155
+ )
156
+ has_substantial_overlap = True
157
  break
158
 
159
  # Calculate overlap amount