Bayan AI commited on
Commit ·
8f8ed51
1
Parent(s): e661f43
Fix collision resolution and punctuation patch application errors
Browse files- src/app.py +63 -8
- 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) =
|
| 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 |
-
|
| 2578 |
-
for
|
| 2579 |
-
_safe_punc = (_safe_punc[:
|
| 2580 |
-
|
| 2581 |
-
_safe_punc[
|
| 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:
|
| 125 |
-
#
|
| 126 |
-
#
|
| 127 |
-
#
|
| 128 |
-
|
| 129 |
-
|
| 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 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
| 148 |
logger.info(
|
| 149 |
-
f"[OVERLAP] Merged punctuation into
|
| 150 |
-
f"[{cs}:{ce}]: '{
|
| 151 |
-
f"'{
|
| 152 |
)
|
| 153 |
-
has_substantial_overlap = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|