Commit ·
2702d02
1
Parent(s): 0898117
fix: contextual logging, word merge overlap resolver, and responsive toolbar/footer UI
Browse files- src/css/components.css +17 -5
- src/nlp/correction_patch.py +19 -1
- src/nlp/spelling/araspell_rules.py +11 -2
- tests/test_bug_fixes.py +5 -5
- tests/test_recent_fixes.py +10 -6
src/css/components.css
CHANGED
|
@@ -413,11 +413,13 @@
|
|
| 413 |
/* ── Formatting Toolbar ── */
|
| 414 |
.format-toolbar {
|
| 415 |
display: flex;
|
| 416 |
-
flex-wrap:
|
| 417 |
align-items: center;
|
| 418 |
justify-content: center;
|
| 419 |
gap: 3px;
|
| 420 |
padding: 6px 10px;
|
|
|
|
|
|
|
| 421 |
border-bottom: 1px solid var(--color-border);
|
| 422 |
background: var(--color-surface);
|
| 423 |
overflow: visible;
|
|
@@ -426,9 +428,13 @@
|
|
| 426 |
flex-shrink: 0;
|
| 427 |
}
|
| 428 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
.fmt-group {
|
| 430 |
display: flex;
|
| 431 |
-
flex-wrap:
|
| 432 |
align-items: center;
|
| 433 |
gap: 1px;
|
| 434 |
background: var(--color-border);
|
|
@@ -687,18 +693,23 @@
|
|
| 687 |
|
| 688 |
.editor-footer {
|
| 689 |
display: flex;
|
| 690 |
-
flex-wrap:
|
| 691 |
align-items: center;
|
| 692 |
justify-content: space-between;
|
| 693 |
gap: var(--spacing-md);
|
| 694 |
padding: var(--spacing-md);
|
| 695 |
border-top: 1px solid var(--color-border);
|
| 696 |
background: var(--color-surface);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
}
|
| 698 |
|
| 699 |
.editor-stats {
|
| 700 |
display: flex;
|
| 701 |
-
flex-wrap:
|
| 702 |
gap: var(--spacing-md);
|
| 703 |
align-items: center;
|
| 704 |
}
|
|
@@ -724,7 +735,7 @@
|
|
| 724 |
|
| 725 |
.editor-actions {
|
| 726 |
display: flex;
|
| 727 |
-
flex-wrap:
|
| 728 |
gap: var(--spacing-sm);
|
| 729 |
}
|
| 730 |
|
|
@@ -737,6 +748,7 @@
|
|
| 737 |
display: inline-flex;
|
| 738 |
align-items: center;
|
| 739 |
justify-content: center;
|
|
|
|
| 740 |
}
|
| 741 |
|
| 742 |
/* ── Buttons ── */
|
|
|
|
| 413 |
/* ── Formatting Toolbar ── */
|
| 414 |
.format-toolbar {
|
| 415 |
display: flex;
|
| 416 |
+
flex-wrap: nowrap;
|
| 417 |
align-items: center;
|
| 418 |
justify-content: center;
|
| 419 |
gap: 3px;
|
| 420 |
padding: 6px 10px;
|
| 421 |
+
overflow-x: auto;
|
| 422 |
+
scrollbar-width: none; /* Hide scrollbar Firefox */
|
| 423 |
border-bottom: 1px solid var(--color-border);
|
| 424 |
background: var(--color-surface);
|
| 425 |
overflow: visible;
|
|
|
|
| 428 |
flex-shrink: 0;
|
| 429 |
}
|
| 430 |
|
| 431 |
+
.format-toolbar::-webkit-scrollbar {
|
| 432 |
+
display: none;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
.fmt-group {
|
| 436 |
display: flex;
|
| 437 |
+
flex-wrap: nowrap;
|
| 438 |
align-items: center;
|
| 439 |
gap: 1px;
|
| 440 |
background: var(--color-border);
|
|
|
|
| 693 |
|
| 694 |
.editor-footer {
|
| 695 |
display: flex;
|
| 696 |
+
flex-wrap: nowrap;
|
| 697 |
align-items: center;
|
| 698 |
justify-content: space-between;
|
| 699 |
gap: var(--spacing-md);
|
| 700 |
padding: var(--spacing-md);
|
| 701 |
border-top: 1px solid var(--color-border);
|
| 702 |
background: var(--color-surface);
|
| 703 |
+
overflow-x: auto;
|
| 704 |
+
scrollbar-width: none;
|
| 705 |
+
}
|
| 706 |
+
.editor-footer::-webkit-scrollbar {
|
| 707 |
+
display: none;
|
| 708 |
}
|
| 709 |
|
| 710 |
.editor-stats {
|
| 711 |
display: flex;
|
| 712 |
+
flex-wrap: nowrap;
|
| 713 |
gap: var(--spacing-md);
|
| 714 |
align-items: center;
|
| 715 |
}
|
|
|
|
| 735 |
|
| 736 |
.editor-actions {
|
| 737 |
display: flex;
|
| 738 |
+
flex-wrap: nowrap;
|
| 739 |
gap: var(--spacing-sm);
|
| 740 |
}
|
| 741 |
|
|
|
|
| 748 |
display: inline-flex;
|
| 749 |
align-items: center;
|
| 750 |
justify-content: center;
|
| 751 |
+
flex-shrink: 0;
|
| 752 |
}
|
| 753 |
|
| 754 |
/* ── Buttons ── */
|
src/nlp/correction_patch.py
CHANGED
|
@@ -16,7 +16,7 @@ from dataclasses import dataclass, field
|
|
| 16 |
|
| 17 |
logger = logging.getLogger(__name__)
|
| 18 |
|
| 19 |
-
PRIORITY = {'autocomplete': 0, '
|
| 20 |
|
| 21 |
|
| 22 |
@dataclass
|
|
@@ -131,6 +131,7 @@ class PatchSet:
|
|
| 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):])):
|
|
@@ -143,6 +144,23 @@ class PatchSet:
|
|
| 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)
|
|
|
|
| 16 |
|
| 17 |
logger = logging.getLogger(__name__)
|
| 18 |
|
| 19 |
+
PRIORITY = {'autocomplete': 0, 'punctuation': 1, 'spelling': 2, 'grammar': 3}
|
| 20 |
|
| 21 |
|
| 22 |
@dataclass
|
|
|
|
| 131 |
prev_correction = claimed_patch.replacement
|
| 132 |
|
| 133 |
# Check if punctuation is just appending trailing punctuation
|
| 134 |
+
# Scenario A: Exact match merge (prev_correction is prefix)
|
| 135 |
if (len(punc_correction) > len(prev_correction)
|
| 136 |
and punc_correction.startswith(prev_correction)
|
| 137 |
and all(c in _PUNCT_CHARS for c in punc_correction[len(prev_correction):])):
|
|
|
|
| 144 |
has_substantial_overlap = True
|
| 145 |
break
|
| 146 |
|
| 147 |
+
# Scenario B: Punctuation just adds punct to its own original text
|
| 148 |
+
# (e.g. original='المدرسة', replacement='المدرسة.', but prev_correction is a split like 'في المدرسة')
|
| 149 |
+
if (len(punc_correction) > len(patch.original)
|
| 150 |
+
and punc_correction.startswith(patch.original)
|
| 151 |
+
and all(c in _PUNCT_CHARS for c in punc_correction[len(patch.original):])):
|
| 152 |
+
added_punct = punc_correction[len(patch.original):]
|
| 153 |
+
# Only append if it doesn't already end with that punct
|
| 154 |
+
if not claimed_patch.replacement.endswith(added_punct):
|
| 155 |
+
claimed_patch.replacement += added_punct
|
| 156 |
+
logger.info(
|
| 157 |
+
f"[OVERLAP] Appended trailing punctuation into {claimed_stage} "
|
| 158 |
+
f"[{cs}:{ce}]: '{claimed_patch.original}' → "
|
| 159 |
+
f"'{claimed_patch.replacement}'"
|
| 160 |
+
)
|
| 161 |
+
has_substantial_overlap = True
|
| 162 |
+
break
|
| 163 |
+
|
| 164 |
# Check if punctuation is just prepending leading punctuation
|
| 165 |
if (len(punc_correction) > len(prev_correction)
|
| 166 |
and punc_correction.endswith(prev_correction)
|
src/nlp/spelling/araspell_rules.py
CHANGED
|
@@ -1175,14 +1175,23 @@ class ArabicSpellChecker:
|
|
| 1175 |
self.use_contextual = use_contextual
|
| 1176 |
if use_contextual:
|
| 1177 |
try:
|
|
|
|
|
|
|
| 1178 |
self.contextual = ContextualCorrector()
|
| 1179 |
-
logger.info("
|
|
|
|
|
|
|
|
|
|
| 1180 |
except Exception as e:
|
| 1181 |
-
logger.warning(
|
|
|
|
|
|
|
|
|
|
| 1182 |
self.contextual = None
|
| 1183 |
self.use_contextual = False
|
| 1184 |
else:
|
| 1185 |
self.contextual = None
|
|
|
|
| 1186 |
|
| 1187 |
def _fix_repeated_end_chars(self, text: str) -> str:
|
| 1188 |
text = re.sub(r'([ا-ي])\1+\b', r'\1', text)
|
|
|
|
| 1175 |
self.use_contextual = use_contextual
|
| 1176 |
if use_contextual:
|
| 1177 |
try:
|
| 1178 |
+
logger.info("=" * 60)
|
| 1179 |
+
logger.info("[MLM/CONTEXTUAL] Loading AraBERT MLM model...")
|
| 1180 |
self.contextual = ContextualCorrector()
|
| 1181 |
+
logger.info("[MLM/CONTEXTUAL] ✅ LOADED SUCCESSFULLY")
|
| 1182 |
+
logger.info(f"[MLM/CONTEXTUAL] Device: {self.contextual.device}")
|
| 1183 |
+
logger.info(f"[MLM/CONTEXTUAL] Vocab size: {len(self.contextual.vocab)}")
|
| 1184 |
+
logger.info("=" * 60)
|
| 1185 |
except Exception as e:
|
| 1186 |
+
logger.warning("=" * 60)
|
| 1187 |
+
logger.warning(f"[MLM/CONTEXTUAL] ❌ FAILED TO LOAD: {e}")
|
| 1188 |
+
logger.warning("[MLM/CONTEXTUAL] Spelling will work without contextual validation")
|
| 1189 |
+
logger.warning("=" * 60)
|
| 1190 |
self.contextual = None
|
| 1191 |
self.use_contextual = False
|
| 1192 |
else:
|
| 1193 |
self.contextual = None
|
| 1194 |
+
logger.info("[MLM/CONTEXTUAL] Disabled by configuration (use_contextual=False)")
|
| 1195 |
|
| 1196 |
def _fix_repeated_end_chars(self, text: str) -> str:
|
| 1197 |
text = re.sub(r'([ا-ي])\1+\b', r'\1', text)
|
tests/test_bug_fixes.py
CHANGED
|
@@ -207,13 +207,13 @@ class TestGrammarSanityCheck(unittest.TestCase):
|
|
| 207 |
"""
|
| 208 |
|
| 209 |
def test_sanity_check_pattern_exists(self):
|
| 210 |
-
"""
|
| 211 |
-
|
| 212 |
-
with open(
|
| 213 |
content = f.read()
|
| 214 |
# Check for the Phase 4 guard comment and logic
|
| 215 |
self.assertIn('Phase 4 (BUG-033/E10)', content,
|
| 216 |
-
"Phase 4 grammar sanity check not found in
|
| 217 |
self.assertIn('Rejected corruption', content,
|
| 218 |
"Grammar corruption rejection log not found")
|
| 219 |
|
|
@@ -433,7 +433,7 @@ class TestLongInputPattern(unittest.TestCase):
|
|
| 433 |
app_path = os.path.join(os.path.dirname(__file__), '..', 'src', 'app.py')
|
| 434 |
with open(app_path, 'r', encoding='utf-8') as f:
|
| 435 |
content = f.read()
|
| 436 |
-
self.assertIn('text_len <=
|
| 437 |
self.assertIn('skipping AraSpell', content)
|
| 438 |
|
| 439 |
|
|
|
|
| 207 |
"""
|
| 208 |
|
| 209 |
def test_sanity_check_pattern_exists(self):
|
| 210 |
+
"""grammar_rules.py must contain the IV/OOV sanity check."""
|
| 211 |
+
target_path = os.path.join(os.path.dirname(__file__), '..', 'src', 'nlp', 'grammar', 'grammar_rules.py')
|
| 212 |
+
with open(target_path, 'r', encoding='utf-8') as f:
|
| 213 |
content = f.read()
|
| 214 |
# Check for the Phase 4 guard comment and logic
|
| 215 |
self.assertIn('Phase 4 (BUG-033/E10)', content,
|
| 216 |
+
"Phase 4 grammar sanity check not found in grammar_rules.py")
|
| 217 |
self.assertIn('Rejected corruption', content,
|
| 218 |
"Grammar corruption rejection log not found")
|
| 219 |
|
|
|
|
| 433 |
app_path = os.path.join(os.path.dirname(__file__), '..', 'src', 'app.py')
|
| 434 |
with open(app_path, 'r', encoding='utf-8') as f:
|
| 435 |
content = f.read()
|
| 436 |
+
self.assertIn('text_len <= 1000', content)
|
| 437 |
self.assertIn('skipping AraSpell', content)
|
| 438 |
|
| 439 |
|
tests/test_recent_fixes.py
CHANGED
|
@@ -250,13 +250,13 @@ ps2.add(CorrectionPatch(
|
|
| 250 |
priority=PRIORITY['punctuation'], confidence=0.8
|
| 251 |
))
|
| 252 |
resolved2 = ps2.resolve_overlaps()
|
| 253 |
-
test("Case 2: punc
|
| 254 |
if resolved2:
|
| 255 |
-
test("
|
| 256 |
-
resolved2[0].replacement == 'ذهبن',
|
| 257 |
f"got '{resolved2[0].replacement}'")
|
| 258 |
|
| 259 |
-
# Case 3: spelling + punctuation
|
| 260 |
ps3 = PatchSet()
|
| 261 |
ps3.add(CorrectionPatch(
|
| 262 |
stage='spelling', start_original=0, end_original=5,
|
|
@@ -271,8 +271,12 @@ ps3.add(CorrectionPatch(
|
|
| 271 |
priority=PRIORITY['punctuation'], confidence=0.8
|
| 272 |
))
|
| 273 |
resolved3 = ps3.resolve_overlaps()
|
| 274 |
-
test("Case 3: spelling+punc
|
| 275 |
-
len(resolved3) ==
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
|
| 278 |
# ══════════════════════════════════════════════════════════════
|
|
|
|
| 250 |
priority=PRIORITY['punctuation'], confidence=0.8
|
| 251 |
))
|
| 252 |
resolved2 = ps2.resolve_overlaps()
|
| 253 |
+
test("Case 2: punc merged", len(resolved2) == 1, f"got {len(resolved2)}")
|
| 254 |
if resolved2:
|
| 255 |
+
test("Appended punc to grammar 'ذهبن.'",
|
| 256 |
+
resolved2[0].replacement == 'ذهبن.',
|
| 257 |
f"got '{resolved2[0].replacement}'")
|
| 258 |
|
| 259 |
+
# Case 3: spelling + punctuation merge (Phase 14 fix)
|
| 260 |
ps3 = PatchSet()
|
| 261 |
ps3.add(CorrectionPatch(
|
| 262 |
stage='spelling', start_original=0, end_original=5,
|
|
|
|
| 271 |
priority=PRIORITY['punctuation'], confidence=0.8
|
| 272 |
))
|
| 273 |
resolved3 = ps3.resolve_overlaps()
|
| 274 |
+
test("Case 3: spelling+punc merged (1 patch)",
|
| 275 |
+
len(resolved3) == 1, f"got {len(resolved3)}")
|
| 276 |
+
if resolved3:
|
| 277 |
+
test("Appended punc to spelling 'برغم.'",
|
| 278 |
+
resolved3[0].replacement == 'برغم.',
|
| 279 |
+
f"got '{resolved3[0].replacement}'")
|
| 280 |
|
| 281 |
|
| 282 |
# ══════════════════════════════════════════════════════════════
|