Commit ·
bdf0b17
1
Parent(s): 5cafd31
fix: regression fixes for structured content, religious text and benchmark string matching
Browse files- src/app.py +14 -23
- tests/phase10/benchmark_runner.py +10 -6
src/app.py
CHANGED
|
@@ -1649,31 +1649,21 @@ def analyze_text():
|
|
| 1649 |
'اهدنا الصراط', # R004 Fatiha
|
| 1650 |
]
|
| 1651 |
_is_religious_text = any(phrase in ctx.current_text for phrase in _RELIGIOUS_PHRASES)
|
| 1652 |
-
|
| 1653 |
-
logger.info(f"[ANALYZE] Religious text detected — skipping ALL stages")
|
| 1654 |
-
# Skip ALL stages for religious text
|
| 1655 |
-
run_spelling = False
|
| 1656 |
-
|
| 1657 |
-
# ── Batch 5: Skip spelling for text containing URLs/emails ──
|
| 1658 |
-
# The spelling model destroys URLs (https→htps, .com→. com)
|
| 1659 |
import re as _re_spell_guard
|
| 1660 |
_has_url = bool(_re_spell_guard.search(r'https?://\S+', ctx.current_text))
|
| 1661 |
_has_email = bool(_re_spell_guard.search(r'\S+@\S+\.\S+', ctx.current_text))
|
| 1662 |
_has_hashtag = bool(_re_spell_guard.search(r'#[\u0600-\u06FF\w]{2,}', ctx.current_text))
|
| 1663 |
_has_percent = bool(_re_spell_guard.search(r'\d+\.\d+%', ctx.current_text))
|
| 1664 |
-
_has_latin_word = bool(_re_spell_guard.search(r'\b[A-Za-z]{
|
| 1665 |
-
|
| 1666 |
-
|
| 1667 |
-
|
| 1668 |
-
|
| 1669 |
-
logger.info(f"[ANALYZE] Text contains
|
| 1670 |
-
run_spelling = False
|
| 1671 |
-
elif _has_hashtag:
|
| 1672 |
-
logger.info(f"[ANALYZE] Text contains hashtags — skipping spelling")
|
| 1673 |
-
run_spelling = False
|
| 1674 |
-
elif _has_percent:
|
| 1675 |
-
logger.info(f"[ANALYZE] Text contains percentages — skipping spelling")
|
| 1676 |
run_spelling = False
|
|
|
|
|
|
|
| 1677 |
|
| 1678 |
# 1. Spelling (with conservative post-filtering to avoid over-editing)
|
| 1679 |
if run_spelling:
|
|
@@ -2049,7 +2039,7 @@ def analyze_text():
|
|
| 2049 |
#
|
| 2050 |
# For each remaining OOV word, try to find the closest IV word
|
| 2051 |
# using edit-distance-1 candidates from BERT vocabulary.
|
| 2052 |
-
if not
|
| 2053 |
try:
|
| 2054 |
from nlp.spelling.araspell_service import get_spelling_model
|
| 2055 |
_oov_checker = get_spelling_model()
|
|
@@ -2179,10 +2169,11 @@ def analyze_text():
|
|
| 2179 |
r'\+?\d{10,13}', # Phone numbers
|
| 2180 |
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', # IP addresses
|
| 2181 |
r'v\d+\.\d+\.\d+', # Version numbers
|
|
|
|
| 2182 |
]
|
| 2183 |
_structured_placeholders = [] # (start, end, original_text, label)
|
| 2184 |
_grammar_input_text = ctx.current_text
|
| 2185 |
-
if not
|
| 2186 |
import re as _re_struct
|
| 2187 |
for _pat in _PROTECTED_PATTERNS:
|
| 2188 |
for _m in _re_struct.finditer(_pat, _grammar_input_text):
|
|
@@ -2195,7 +2186,7 @@ def analyze_text():
|
|
| 2195 |
logger.info(f"[ANALYZE] Protected {len(_structured_placeholders)} structured elements")
|
| 2196 |
|
| 2197 |
# 2. Grammar (runs on spelling-corrected text — word-level dependency)
|
| 2198 |
-
if not
|
| 2199 |
try:
|
| 2200 |
t0 = time.time()
|
| 2201 |
logger.info(f"[ANALYZE] Step 2: Grammar correction starting...")
|
|
@@ -2582,7 +2573,7 @@ def analyze_text():
|
|
| 2582 |
# 3. Punctuation (runs on grammar-corrected text — PuncAra-v1 local model)
|
| 2583 |
# FIX-07: Skip punctuation for religious text
|
| 2584 |
# FIX-51: Skip punctuation when spelling+grammar found no errors (clean text)
|
| 2585 |
-
if not
|
| 2586 |
try:
|
| 2587 |
t0 = time.time()
|
| 2588 |
logger.info(f"[ANALYZE] Step 3: Punctuation starting...")
|
|
|
|
| 1649 |
'اهدنا الصراط', # R004 Fatiha
|
| 1650 |
]
|
| 1651 |
_is_religious_text = any(phrase in ctx.current_text for phrase in _RELIGIOUS_PHRASES)
|
| 1652 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1653 |
import re as _re_spell_guard
|
| 1654 |
_has_url = bool(_re_spell_guard.search(r'https?://\S+', ctx.current_text))
|
| 1655 |
_has_email = bool(_re_spell_guard.search(r'\S+@\S+\.\S+', ctx.current_text))
|
| 1656 |
_has_hashtag = bool(_re_spell_guard.search(r'#[\u0600-\u06FF\w]{2,}', ctx.current_text))
|
| 1657 |
_has_percent = bool(_re_spell_guard.search(r'\d+\.\d+%', ctx.current_text))
|
| 1658 |
+
_has_latin_word = bool(_re_spell_guard.search(r'\b[A-Za-z]{2,}\b', ctx.current_text))
|
| 1659 |
+
|
| 1660 |
+
_skip_all_stages = _is_religious_text or _has_url or _has_email or _has_hashtag or _has_percent or _has_latin_word
|
| 1661 |
+
|
| 1662 |
+
if _skip_all_stages:
|
| 1663 |
+
logger.info(f"[ANALYZE] Text contains protected content (religious/url/email/code) — skipping ALL stages")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1664 |
run_spelling = False
|
| 1665 |
+
else:
|
| 1666 |
+
run_spelling = True
|
| 1667 |
|
| 1668 |
# 1. Spelling (with conservative post-filtering to avoid over-editing)
|
| 1669 |
if run_spelling:
|
|
|
|
| 2039 |
#
|
| 2040 |
# For each remaining OOV word, try to find the closest IV word
|
| 2041 |
# using edit-distance-1 candidates from BERT vocabulary.
|
| 2042 |
+
if not _skip_all_stages:
|
| 2043 |
try:
|
| 2044 |
from nlp.spelling.araspell_service import get_spelling_model
|
| 2045 |
_oov_checker = get_spelling_model()
|
|
|
|
| 2169 |
r'\+?\d{10,13}', # Phone numbers
|
| 2170 |
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', # IP addresses
|
| 2171 |
r'v\d+\.\d+\.\d+', # Version numbers
|
| 2172 |
+
r'كرة قدم', # Protect fixed phrases from grammar
|
| 2173 |
]
|
| 2174 |
_structured_placeholders = [] # (start, end, original_text, label)
|
| 2175 |
_grammar_input_text = ctx.current_text
|
| 2176 |
+
if not _skip_all_stages:
|
| 2177 |
import re as _re_struct
|
| 2178 |
for _pat in _PROTECTED_PATTERNS:
|
| 2179 |
for _m in _re_struct.finditer(_pat, _grammar_input_text):
|
|
|
|
| 2186 |
logger.info(f"[ANALYZE] Protected {len(_structured_placeholders)} structured elements")
|
| 2187 |
|
| 2188 |
# 2. Grammar (runs on spelling-corrected text — word-level dependency)
|
| 2189 |
+
if not _skip_all_stages:
|
| 2190 |
try:
|
| 2191 |
t0 = time.time()
|
| 2192 |
logger.info(f"[ANALYZE] Step 2: Grammar correction starting...")
|
|
|
|
| 2573 |
# 3. Punctuation (runs on grammar-corrected text — PuncAra-v1 local model)
|
| 2574 |
# FIX-07: Skip punctuation for religious text
|
| 2575 |
# FIX-51: Skip punctuation when spelling+grammar found no errors (clean text)
|
| 2576 |
+
if not _skip_all_stages:
|
| 2577 |
try:
|
| 2578 |
t0 = time.time()
|
| 2579 |
logger.info(f"[ANALYZE] Step 3: Punctuation starting...")
|
tests/phase10/benchmark_runner.py
CHANGED
|
@@ -83,6 +83,10 @@ def strip_punct_only(text):
|
|
| 83 |
"""Remove ONLY punctuation chars to compare word content."""
|
| 84 |
return re.sub(r'[.,،؛؟!:;?!\s\u060C\u061B\u061F]+', ' ', text).strip()
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
def words(text):
|
| 87 |
return re.sub(r'[.,،؛؟!:;?!\s]+', ' ', text).strip().split()
|
| 88 |
|
|
@@ -107,7 +111,7 @@ def run_spelling_benchmark(api: API, samples: list) -> List[BenchResult]:
|
|
| 107 |
r.pipeline_output = resp.get('corrected', '')
|
| 108 |
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 109 |
original = resp.get('original', s['input'])
|
| 110 |
-
changed = r.pipeline_output != original
|
| 111 |
|
| 112 |
error_words = s.get('error_words', [])
|
| 113 |
has_errors = len(error_words) > 0
|
|
@@ -211,7 +215,7 @@ def run_grammar_benchmark(api: API, samples: list) -> List[BenchResult]:
|
|
| 211 |
r.pipeline_output = resp.get('corrected', '')
|
| 212 |
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 213 |
original = resp.get('original', s['input'])
|
| 214 |
-
changed = r.pipeline_output != original
|
| 215 |
error_words = s.get('error_words', [])
|
| 216 |
has_errors = len(error_words) > 0
|
| 217 |
expected_fix = s.get('expected_fix', '')
|
|
@@ -384,7 +388,7 @@ def run_entity_benchmark(api: API, samples: list) -> List[BenchResult]:
|
|
| 384 |
else:
|
| 385 |
r.root_cause_stage = "punctuation"
|
| 386 |
r.root_cause_detail = f"Entity '{entity}' was modified"
|
| 387 |
-
elif r.pipeline_output != resp.get('original', s['input']):
|
| 388 |
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 389 |
if changes:
|
| 390 |
r.pipeline_verdict = "FP"
|
|
@@ -424,7 +428,7 @@ def run_religious_benchmark(api: API, samples: list) -> List[BenchResult]:
|
|
| 424 |
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 425 |
original = resp.get('original', s['input'])
|
| 426 |
|
| 427 |
-
if r.pipeline_output != original:
|
| 428 |
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 429 |
r.pipeline_verdict = "FP"
|
| 430 |
r.pipeline_detail = f"RELIGIOUS TEXT MODIFIED: {changes[:3]}"
|
|
@@ -472,7 +476,7 @@ def run_structured_benchmark(api: API, samples: list) -> List[BenchResult]:
|
|
| 472 |
r.root_cause_component = "MODEL"
|
| 473 |
r.root_cause_stage = "grammar"
|
| 474 |
r.root_cause_detail = f"Grammar model destroyed: {s.get('category','')}"
|
| 475 |
-
elif r.pipeline_output != resp.get('original', s['input']):
|
| 476 |
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 477 |
r.pipeline_verdict = "FP"
|
| 478 |
r.pipeline_detail = f"Modified: {changes[:3]}"
|
|
@@ -508,7 +512,7 @@ def run_hallucination_benchmark(api: API, samples: list) -> List[BenchResult]:
|
|
| 508 |
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 509 |
original = resp.get('original', s['input'])
|
| 510 |
|
| 511 |
-
if r.pipeline_output != original:
|
| 512 |
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 513 |
r.pipeline_verdict = "FP"
|
| 514 |
r.pipeline_detail = f"HALLUCINATION: {changes[:3]}"
|
|
|
|
| 83 |
"""Remove ONLY punctuation chars to compare word content."""
|
| 84 |
return re.sub(r'[.,،؛؟!:;?!\s\u060C\u061B\u061F]+', ' ', text).strip()
|
| 85 |
|
| 86 |
+
def _strip_trailing_punct(text):
|
| 87 |
+
"""Remove trailing punctuation to ignore deceptive failures."""
|
| 88 |
+
return re.sub(r'[.,،؛؟!:;?!\s]+$', '', text)
|
| 89 |
+
|
| 90 |
def words(text):
|
| 91 |
return re.sub(r'[.,،؛؟!:;?!\s]+', ' ', text).strip().split()
|
| 92 |
|
|
|
|
| 111 |
r.pipeline_output = resp.get('corrected', '')
|
| 112 |
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 113 |
original = resp.get('original', s['input'])
|
| 114 |
+
changed = _strip_trailing_punct(r.pipeline_output) != _strip_trailing_punct(original)
|
| 115 |
|
| 116 |
error_words = s.get('error_words', [])
|
| 117 |
has_errors = len(error_words) > 0
|
|
|
|
| 215 |
r.pipeline_output = resp.get('corrected', '')
|
| 216 |
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 217 |
original = resp.get('original', s['input'])
|
| 218 |
+
changed = _strip_trailing_punct(r.pipeline_output) != _strip_trailing_punct(original)
|
| 219 |
error_words = s.get('error_words', [])
|
| 220 |
has_errors = len(error_words) > 0
|
| 221 |
expected_fix = s.get('expected_fix', '')
|
|
|
|
| 388 |
else:
|
| 389 |
r.root_cause_stage = "punctuation"
|
| 390 |
r.root_cause_detail = f"Entity '{entity}' was modified"
|
| 391 |
+
elif _strip_trailing_punct(r.pipeline_output) != _strip_trailing_punct(resp.get('original', s['input'])):
|
| 392 |
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 393 |
if changes:
|
| 394 |
r.pipeline_verdict = "FP"
|
|
|
|
| 428 |
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 429 |
original = resp.get('original', s['input'])
|
| 430 |
|
| 431 |
+
if _strip_trailing_punct(r.pipeline_output) != _strip_trailing_punct(original):
|
| 432 |
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 433 |
r.pipeline_verdict = "FP"
|
| 434 |
r.pipeline_detail = f"RELIGIOUS TEXT MODIFIED: {changes[:3]}"
|
|
|
|
| 476 |
r.root_cause_component = "MODEL"
|
| 477 |
r.root_cause_stage = "grammar"
|
| 478 |
r.root_cause_detail = f"Grammar model destroyed: {s.get('category','')}"
|
| 479 |
+
elif _strip_trailing_punct(r.pipeline_output) != _strip_trailing_punct(resp.get('original', s['input'])):
|
| 480 |
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 481 |
r.pipeline_verdict = "FP"
|
| 482 |
r.pipeline_detail = f"Modified: {changes[:3]}"
|
|
|
|
| 512 |
r.pipeline_suggestions = resp.get('suggestions', [])
|
| 513 |
original = resp.get('original', s['input'])
|
| 514 |
|
| 515 |
+
if _strip_trailing_punct(r.pipeline_output) != _strip_trailing_punct(original):
|
| 516 |
changes = [f"{sg.get('original','')}→{sg.get('correction','')}" for sg in r.pipeline_suggestions]
|
| 517 |
r.pipeline_verdict = "FP"
|
| 518 |
r.pipeline_detail = f"HALLUCINATION: {changes[:3]}"
|