Bayan AI commited on
Commit
76b9ec3
·
1 Parent(s): dd8a53f

Fix: TerminalPunctuationGuard for short phrases and comprehensive Nasb/Jazm grammar rules fixes

Browse files
apply_locks.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ def apply_lock_to_file(filepath, var_name, engine_name, func_name):
4
+ with open(filepath, 'r', encoding='utf-8') as f:
5
+ lines = f.readlines()
6
+
7
+ out_lines = []
8
+ in_imports = False
9
+ added_threading = False
10
+ in_globals = False
11
+ added_lock_var = False
12
+ in_func = False
13
+
14
+ for line in lines:
15
+ if line.startswith('import ') and not added_threading:
16
+ out_lines.append(line)
17
+ out_lines.append("import threading\n")
18
+ added_threading = True
19
+ continue
20
+
21
+ if line.startswith(f'_{var_name} = None') and not added_lock_var:
22
+ out_lines.append(line)
23
+ out_lines.append(f"_load_lock = threading.Lock()\n")
24
+ added_lock_var = True
25
+ continue
26
+
27
+ if line.startswith(f'def {func_name}('):
28
+ in_func = True
29
+ out_lines.append(line)
30
+ continue
31
+
32
+ if in_func:
33
+ if line.startswith(f' global '):
34
+ out_lines.append(line.replace('\n', f', _load_lock\n'))
35
+ continue
36
+
37
+ if line.startswith(f' try:'):
38
+ # The start of the old try block. We wrap everything from here.
39
+ out_lines.append(f' with _load_lock:\n')
40
+ out_lines.append(f' if _{var_name} is not None:\n')
41
+ out_lines.append(f' return _{var_name}\n\n')
42
+ out_lines.append(f' try:\n')
43
+ continue
44
+
45
+ # If we are inside the function and past the global declaration,
46
+ # and it's indented with at least 4 spaces, we need to add 4 more spaces
47
+ # for the lines that were inside the old `try:` and `except:`
48
+ # EXCEPT for `if _xxx is not None: return _xxx` which comes before the try
49
+ if line.startswith(' if _') or line.startswith(' return _'):
50
+ # This is the old `if checker is not None:` logic before try. Leave it alone.
51
+ out_lines.append(line)
52
+ continue
53
+
54
+ if line.startswith(' '):
55
+ # Shift everything that was inside try/except right by 4 spaces
56
+ if line.strip() == '':
57
+ out_lines.append('\n')
58
+ else:
59
+ out_lines.append(' ' + line)
60
+
61
+ if line.startswith(' return _') or line.startswith(' raise RuntimeError'):
62
+ # End of function
63
+ in_func = False
64
+ continue
65
+
66
+ out_lines.append(line)
67
+
68
+ with open(filepath, 'w', encoding='utf-8') as f:
69
+ f.writelines(out_lines)
70
+
71
+
72
+ apply_lock_to_file(r'src/nlp/spelling/araspell_service.py', 'spell_checker', 'AraSpell', 'get_spelling_model')
73
+ apply_lock_to_file(r'src/nlp/punctuation/punctuation_service.py', 'punctuation_checker', 'PuncAra', 'get_punctuation_model')
74
+ apply_lock_to_file(r'src/nlp/grammar/grammar_service.py', 'grammar_checker', 'Grammar', 'get_grammar_model')
75
+ apply_lock_to_file(r'src/nlp/autocomplete/autocomplete_service.py', 'autocomplete_engine', 'Autocomplete', 'get_autocomplete_model')
76
+
77
+ print("Locks applied perfectly with correct indentation!")
debug_pipeline.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ # Add src to python path
5
+ sys.path.insert(0, str(Path(r'c:\Users\dell\PycharmProjects\JupyterProject1\PythonProject\BAYAN\src')))
6
+
7
+ from app import app
8
+ import json
9
+
10
+ client = app.test_client()
11
+
12
+ def test(text):
13
+ print(f"\n--- Testing: {text} ---")
14
+ resp = client.post('/api/analyze', json={'text': text})
15
+ data = resp.get_json()
16
+ if 'suggestions' in data:
17
+ for s in data['suggestions']:
18
+ print(f"[{s['type'].upper()}] '{s['original']}' -> '{s['correction']}'")
19
+ else:
20
+ print("Error:", data)
21
+
22
+ test("ذهبت المهندسون الي العمل")
23
+ test("ذهبت المهندسون")
24
+ test("الي العمل")
debug_punctuation.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import io
3
+ from pathlib import Path
4
+
5
+ # Force UTF-8 encoding for standard output
6
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
7
+
8
+ # Add src to python path
9
+ sys.path.insert(0, str(Path(r'c:\Users\dell\PycharmProjects\JupyterProject1\PythonProject\BAYAN\src')))
10
+
11
+ from app import app
12
+
13
+ client = app.test_client()
14
+
15
+ def test(text):
16
+ print(f"\n--- Testing: {text} ---")
17
+ resp = client.post('/api/analyze', json={'text': text})
18
+ data = resp.get_json()
19
+
20
+ if data and 'corrected' in data:
21
+ print(f"Corrected: {data['corrected']}")
22
+
23
+ if data and 'suggestions' in data:
24
+ for s in data['suggestions']:
25
+ print(f"[{s['type'].upper()}] '{s['original']}' -> '{s['correction']}'")
26
+ elif data and 'error' in data:
27
+ print("Error:", data['error'])
28
+ else:
29
+ print("Raw Data:", data)
30
+
31
+ if __name__ == "__main__":
32
+ # Test 1: Single Entity (Should not have punctuation added)
33
+ test("شركة أبل")
34
+
35
+ # Test 2: Short phrase (Should not have punctuation added)
36
+ test("مرحبا بكم في التطبيق")
37
+
38
+ # Test 3: Grammar error that gets fixed but then corrupted by punctuation
39
+ # "الى" is spelled wrong (needs hamza on alif below if it's إِلى or just remains الى depending on rules,
40
+ # but let's see what grammar does). Actually "يذهبون المهندسون" is a grammar error in Arabic
41
+ # (should be يذهب المهندسون).
42
+ test("يذهبون المهندسون الى الشركة")
43
+
44
+ # Test 4: Verify Grammar Model preserves punctuation
45
+ test("يذهبون المهندسون الى الشركة، أليس كذلك؟")
extract_grammar_fails.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import sys
3
+
4
+ sys.stdout.reconfigure(encoding='utf-8')
5
+
6
+ try:
7
+ with open('tests/phase10/reports/phase10_results.json', 'r', encoding='utf-8') as f:
8
+ data = json.load(f)
9
+
10
+ with open('grammar_fails_output.md', 'w', encoding='utf-8') as out_f:
11
+ out_f.write("=== GRAMMAR FALSE NEGATIVES ===\n")
12
+ for r in data.get('results', []):
13
+ if r.get('dataset') == 'grammar' and r.get('pipeline_verdict') == 'FN':
14
+ out_f.write(f"[{r.get('id')}] - {r.get('category')}\n")
15
+ out_f.write(f" IN: {r.get('input')}\n")
16
+ out_f.write(f" EXP: {r.get('expected')}\n")
17
+ out_f.write(f" RAW_GRAM: {r.get('grammar_raw_output')}\n")
18
+ out_f.write(f" FINAL: {r.get('pipeline_output')}\n")
19
+ out_f.write("-" * 50 + "\n")
20
+
21
+
22
+ except Exception as e:
23
+ print(f"Error: {e}")
grammar_fails_output.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === GRAMMAR FALSE NEGATIVES ===
2
+ [G006] - sv_agree
3
+ IN: الأولاد لعب في الحديقة
4
+ EXP: لعبوا
5
+ RAW_GRAM: الأولاد لعبوَ في الحديقة
6
+ FINAL: الأولاد لعب في الحديقة.
7
+ --------------------------------------------------
8
+ [G009] - sv_agree
9
+ IN: العمال بنى المبنى
10
+ EXP: بنوا
11
+ RAW_GRAM: العمال بنى المبنى
12
+ FINAL: العمال بنى المبنى.
13
+ --------------------------------------------------
14
+ [G013] - gender
15
+ IN: الطالبة متفوق في دراسته
16
+ EXP: متفوقة/دراستها
17
+ RAW_GRAM: الطالب متفوق في دراسته
18
+ FINAL: الطالب متفوق في دراسته.
19
+ --------------------------------------------------
20
+ [G022] - five_nouns
21
+ IN: رأيت أخوك في المسجد
22
+ EXP: أخاك
23
+ RAW_GRAM: رأيت أخوك في المسجد
24
+ FINAL: رأيت أخوك في المسجد
25
+ --------------------------------------------------
26
+ [G026] - dual
27
+ IN: هاتان الطالبان مجتهدان
28
+ EXP: هذان
29
+ RAW_GRAM: هذان الطالبان مجتهدان
30
+ FINAL: هاتان الطالبات مجتهدان.
31
+ --------------------------------------------------
32
+ [G028] - nasb
33
+ IN: لم يفعلون الواجب بعد
34
+ EXP: يفعلوا
35
+ RAW_GRAM: لم يفعلوَ الواجب بعد
36
+ FINAL: لم يفعلون الواجب بعد
37
+ --------------------------------------------------
reports/Phase10_Post_IVtoOOV_Audit.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 10 Benchmark Audit — Post IVtoOOV Removal
2
+
3
+ > **Date**: 2026-06-24
4
+ > **Action**: Removed `IVtoOOV` filter and added advanced `camel-tools` POS tagging for dual/plural noun-adjective agreement.
5
+
6
+ ## 1. Top-Level Aggregate Metrics
7
+
8
+ | Metric | Score | Notes |
9
+ |---|---|---|
10
+ | **Overall Pass Rate** | **56.2%** | Massive improvement (previously ~25%) |
11
+ | Total Tests | 320 | |
12
+ | True Positives (TP) | 95 | Successfully fixed real errors |
13
+ | True Negatives (TN) | 85 | Successfully ignored correct text |
14
+ | False Positives (FP) | 79 | Hallucinations or overcorrections |
15
+ | False Negatives (FN) | 61 | Failed to fix real errors |
16
+
17
+ ### Root Cause Analysis (61 FN + 79 FP = 140 Failures)
18
+ - **Punctuation Model (`MODEL:punctuation`)**: 64 failures
19
+ - **Integration/Collisions (`PIPELINE:integration`)**: 32 failures
20
+ - **Spelling Model (`MODEL:spelling`)**: 21 failures
21
+ - **Grammar Model (`MODEL:grammar`)**: 18 failures
22
+
23
+ ---
24
+
25
+ ## 2. Per-Dataset Breakdown
26
+
27
+ ### Grammar Dataset
28
+ * **Pass Rate:** 57.8% (up from 26.7%)
29
+ * **Recall:** 80.0% (up from 40.0%)
30
+ * **Analysis:** Removing `IVtoOOV` successfully unblocked valid grammatical structural changes. The recall doubled.
31
+ * **Remaining Issue:** High False Positive Rate on the `correct...` category. The model hallucinates changes on already perfect text.
32
+
33
+ ### Spelling Dataset
34
+ * **Pass Rate:** 63.7% (up from 42.5%)
35
+ * **Recall:** 79.4%
36
+ * **Remaining Issue:** Still missing some Hamza errors and complex word splits (`عندالباب` -> `عند الباب`).
37
+
38
+ ### Structured Content & Religious Datasets
39
+ * **Structured Pass Rate:** 82.9% (up from 5.7%)
40
+ * **Religious Pass Rate:** 90.0% (up from 10.0%)
41
+ * **Analysis:** The `DigitGuard` and punctuation bypass rules are working incredibly well to protect specialized text.
42
+
43
+ ### Pipeline Collision Dataset
44
+ * **Pass Rate:** 16.0% (Terrible)
45
+ * **False Negative Rate:** 84.0%
46
+ * **Analysis:** When a spelling error is adjacent to a grammar error, `StageLocker` is locking the word and preventing the grammar model from seeing or fixing the grammatical context.
47
+
48
+ ### Entities Dataset
49
+ * **Pass Rate:** 13.3%
50
+ * **Analysis:** The models (especially punctuation and spelling) are aggressively modifying named entities (people, places).
51
+
52
+ ---
53
+
54
+ ## 3. Strategic Action Plan for Enhancements
55
+
56
+ To push the pass rate from **56.2%** to **>80%**, we must address the following critical areas:
57
+
58
+ ### A. Tame the "StageLocker" (Fix Pipeline Collisions)
59
+ The `StageLocker` in `app.py` enforces a rigid "Spelling locks word X, Grammar cannot touch word X" rule. This breaks multi-stage corrections.
60
+ **Solution:** Relax the `StageLocker`. Allow the grammar model to operate on tokens that were modified by spelling, provided the grammatical change doesn't completely revert the spelling correction (e.g., checking Jaccard distance or allowing suffix-only changes to locked words).
61
+
62
+ ### B. Stop Punctuation Hallucinations
63
+ The punctuation model causes **64 failures**, mostly by adding periods `.` or question marks `؟` to the end of short sentences or entities where they don't belong.
64
+ **Solution:** Implement a strict `TerminalPunctuationGuard`. If the original text is < 5 words and doesn't end in punctuation, automatically strip any trailing punctuation added by the model.
65
+
66
+ ### C. Implement Named Entity Recognition (NER) Bypass
67
+ Entities (Person names, Cities) are failing at an 86% rate.
68
+ **Solution:** Integrate `camel-tools` NER (Named Entity Recognition). Scan the input text for `LOC`, `PERS`, and `ORG`. If a word is an entity, add it to a dynamic whitelist so the Spelling and Grammar models skip it entirely.
69
+
70
+ ### D. Tame Grammar Hallucinations on Correct Text
71
+ The grammar model hallucinates on perfectly correct text.
72
+ **Solution:** Use a POS-based confidence score. If the grammar model attempts to change a noun into a verb, or completely alters the POS structure of an already valid sentence, reject the change. Alternatively, enforce stricter `Jaccard_05` checks for non-structural changes.
src/app.py CHANGED
@@ -1101,6 +1101,15 @@ def _is_small_spelling_change(orig_word, corr_word, vocab_manager=None):
1101
  # Exception: if diff is just adding/removing ا at start (hamza)
1102
  if abs(len(orig_word) - len(corr_word)) > 1:
1103
  return 0.0
 
 
 
 
 
 
 
 
 
1104
  # ── Phase 12 (A1): Keyboard-neighbor and phonetic acceptance ──
1105
  # Check each differing character: ortho → full accept, keyboard/phonetic → dampened
1106
  _has_keyboard_or_phonetic = False
 
1101
  # Exception: if diff is just adding/removing ا at start (hamza)
1102
  if abs(len(orig_word) - len(corr_word)) > 1:
1103
  return 0.0
1104
+
1105
+ # ── FIX: Block Grammar Changes masked as Spelling Typos (Dual → Plural) ──
1106
+ if orig_word.endswith('ان') and corr_word.endswith('ات') and orig_word[:-2] == corr_word[:-2]:
1107
+ logger.info(
1108
+ f"[SPELLING] Blocked grammatical change (Dual→Plural): "
1109
+ f"'{orig_word}'→'{corr_word}'"
1110
+ )
1111
+ return 0.0
1112
+
1113
  # ── Phase 12 (A1): Keyboard-neighbor and phonetic acceptance ──
1114
  # Check each differing character: ortho → full accept, keyboard/phonetic → dampened
1115
  _has_keyboard_or_phonetic = False
src/nlp/grammar/grammar_rules.py CHANGED
@@ -118,7 +118,8 @@ class ArabicGrammarGuard:
118
  if prev_word in jazm_particles or word.startswith('ل') or word.startswith('ول'):
119
  is_jazm_context = True
120
 
121
- if pos_tag == 'verb' and (is_nasb_context or is_jazm_context):
 
122
  if word.endswith('ون'):
123
  word = word[:-2] + 'وا'
124
  elif word.endswith('ان'):
@@ -126,12 +127,30 @@ class ArabicGrammarGuard:
126
  elif word.endswith('ين'):
127
  word = word[:-2] + 'ي'
128
  elif is_jazm_context:
129
- if word.endswith('و') and len(word) > 3:
130
  word = word[:-1] + 'ُ'
131
- elif (word.endswith('i') or word.endswith('ي')) and len(word) > 3:
132
- if word.endswith('ي'): word = word[:-1] + 'ِ'
133
- elif (word.endswith('ى') or word.endswith('ا')) and len(word) > 3:
134
- word = word[:-1] + 'َ'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  corrected_tokens.append(word)
137
  return " ".join(corrected_tokens)
@@ -325,15 +344,23 @@ class ArabicGrammarGuard:
325
  elif is_plural_masc:
326
  if (not verb_word.endswith('ون') and not verb_word.endswith('وا')
327
  and not verb_word.endswith('ين')):
 
 
328
  corrected_tokens[i+1] = verb_word + 'ون'
329
  else:
330
  # Past tense: ذهب→ذهبوا (masc) / ذهبن (fem)
331
  if is_plural_fem:
332
  if not verb_word.endswith('ن') and not verb_word.endswith('نَ'):
 
 
333
  corrected_tokens[i+1] = verb_word + 'ن'
334
  elif is_plural_masc:
335
  if (not verb_word.endswith('وا') and not verb_word.endswith('ون')
336
  and not verb_word.endswith('ين')):
 
 
 
 
337
  corrected_tokens[i+1] = verb_word + 'وا'
338
 
339
  return " ".join(corrected_tokens)
@@ -343,6 +370,10 @@ class ArabicGrammarGuard:
343
  text = re.sub(r'\b(إن|أن|كأن|لكن|لعل|ليت)\s+(أبوك|أخوك|ذو|فوك)\b',
344
  lambda m: f"{m.group(1)} {m.group(2).replace('و', 'ا')}", text)
345
 
 
 
 
 
346
  # حروف الجر المنفصلة بمسافة (في أخوك -> في أخيك)
347
  text = re.sub(r'\b([وف]?(?:في|من|إلى|على|عن))\s+(أبوك|أباك|أخوك|أخاك|ذو|ذا)\b',
348
  lambda m: f"{m.group(1)} {m.group(2).replace('و', 'ي').replace('ا', 'ي')}", text)
@@ -509,6 +540,19 @@ class ArabicGrammarGuard:
509
  """Apply all grammar rules to model output."""
510
  text = self.preserve_numbers(original_text, generated_text)
511
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  # Each rule is wrapped in try/except so that if camel-tools
513
  # functions fail, the regex-based rules still execute.
514
  for rule_name, rule_fn in [
@@ -527,8 +571,7 @@ class ArabicGrammarGuard:
527
  text = rule_fn(text)
528
  except Exception as e:
529
  logger.warning(f"[GRAMMAR-RULES] {rule_name} failed: {e}")
530
- # Continue with remaining rules
531
-
532
  text = re.sub(r'\s+', ' ', text).strip()
533
  return text
534
 
 
118
  if prev_word in jazm_particles or word.startswith('ل') or word.startswith('ول'):
119
  is_jazm_context = True
120
 
121
+ is_present_tense = word.startswith('ي') or word.startswith('ت') or word.startswith('ن') or word.startswith('أ')
122
+ if (pos_tag == 'verb' or is_present_tense) and (is_nasb_context or is_jazm_context):
123
  if word.endswith('ون'):
124
  word = word[:-2] + 'وا'
125
  elif word.endswith('ان'):
 
127
  elif word.endswith('ين'):
128
  word = word[:-2] + 'ي'
129
  elif is_jazm_context:
130
+ if word.endswith('و') and len(word) >= 3:
131
  word = word[:-1] + 'ُ'
132
+ elif word.endswith('ي') or word.endswith('i'):
133
+ stem = word[:-1]
134
+ fatha_stems = {'يسع', 'تسع', 'أسع', 'نسع',
135
+ 'يخش', 'تخش', 'أخش', 'نخش',
136
+ 'يرض', 'ترض', 'أرض', 'نرض',
137
+ 'ينس', 'تنس', 'أنس', 'ننس',
138
+ 'يبق', 'تبق', 'أبق', 'نبق',
139
+ 'ير', 'تر', 'أر', 'نر',
140
+ 'يلق', 'تلق', 'ألق', 'نلق',
141
+ 'ينه', 'تنه', 'أنه', 'ننه'}
142
+ if stem in fatha_stems:
143
+ word = stem + 'َ'
144
+ elif len(word) > 3:
145
+ word = stem + 'ِ'
146
+ elif (word.endswith('ى') or word.endswith('ا')) and len(word) >= 3:
147
+ if not word.endswith('وا'):
148
+ word = word[:-1] + 'َ'
149
+ elif is_nasb_context:
150
+ if word.endswith('و') and len(word) > 3:
151
+ word = word + 'َ'
152
+ elif word.endswith('ي') and len(word) > 3:
153
+ word = word + 'َ'
154
 
155
  corrected_tokens.append(word)
156
  return " ".join(corrected_tokens)
 
344
  elif is_plural_masc:
345
  if (not verb_word.endswith('ون') and not verb_word.endswith('وا')
346
  and not verb_word.endswith('ين')):
347
+ if verb_word.endswith('وَ'):
348
+ verb_word = verb_word[:-1]
349
  corrected_tokens[i+1] = verb_word + 'ون'
350
  else:
351
  # Past tense: ذهب→ذهبوا (masc) / ذهبن (fem)
352
  if is_plural_fem:
353
  if not verb_word.endswith('ن') and not verb_word.endswith('نَ'):
354
+ if verb_word.endswith('ى') or verb_word.endswith('ا'):
355
+ verb_word = verb_word[:-1]
356
  corrected_tokens[i+1] = verb_word + 'ن'
357
  elif is_plural_masc:
358
  if (not verb_word.endswith('وا') and not verb_word.endswith('ون')
359
  and not verb_word.endswith('ين')):
360
+ if verb_word.endswith('وَ'):
361
+ verb_word = verb_word[:-1]
362
+ elif verb_word.endswith('ى') or verb_word.endswith('ا'):
363
+ verb_word = verb_word[:-1]
364
  corrected_tokens[i+1] = verb_word + 'وا'
365
 
366
  return " ".join(corrected_tokens)
 
370
  text = re.sub(r'\b(إن|أن|كأن|لكن|لعل|ليت)\s+(أبوك|أخوك|ذو|فوك)\b',
371
  lambda m: f"{m.group(1)} {m.group(2).replace('و', 'ا')}", text)
372
 
373
+ # الأفعال المتعدية (Object position)
374
+ text = re.sub(r'\b(رأيت|شاهدت|قابلت|زرت|سمعت|عرفت|وجدت|أحب|أكرمت|صادفت)\s+(أبوك|أخوك|ذو|فوك)\b',
375
+ lambda m: f"{m.group(1)} {m.group(2).replace('و', 'ا')}", text)
376
+
377
  # حروف الجر المنفصلة بمسافة (في أخوك -> في أخيك)
378
  text = re.sub(r'\b([وف]?(?:في|من|إلى|على|عن))\s+(أبوك|أباك|أخوك|أخاك|ذو|ذا)\b',
379
  lambda m: f"{m.group(1)} {m.group(2).replace('و', 'ي').replace('ا', 'ي')}", text)
 
540
  """Apply all grammar rules to model output."""
541
  text = self.preserve_numbers(original_text, generated_text)
542
 
543
+ # ── Fix Hallucinated Subject Gender ──
544
+ # If model incorrectly changes female subject to male, restore it.
545
+ orig_words = original_text.split()
546
+ corr_words = text.split()
547
+ if len(orig_words) == len(corr_words):
548
+ for i, (o, c) in enumerate(zip(orig_words, corr_words)):
549
+ o_clean = o.rstrip('.,،؛;:!؟?()[]{}«»"\'…')
550
+ c_clean = c.rstrip('.,،؛;:!؟?()[]{}«»"\'…')
551
+ # If model dropped 'ة' from a word of length >= 4
552
+ if o_clean.endswith('ة') and not c_clean.endswith('ة') and o_clean[:-1] == c_clean:
553
+ corr_words[i] = o
554
+ text = " ".join(corr_words)
555
+
556
  # Each rule is wrapped in try/except so that if camel-tools
557
  # functions fail, the regex-based rules still execute.
558
  for rule_name, rule_fn in [
 
571
  text = rule_fn(text)
572
  except Exception as e:
573
  logger.warning(f"[GRAMMAR-RULES] {rule_name} failed: {e}")
574
+
 
575
  text = re.sub(r'\s+', ' ', text).strip()
576
  return text
577
 
src/nlp/punctuation/punctuation_rules.py CHANGED
@@ -149,7 +149,7 @@ def validate_punctuation_diff(diff: dict, full_text: str = '') -> bool:
149
  # Also check for ellipsis (... at end)
150
  _full_has_ellipsis = full_text.rstrip().endswith('...') if full_text else False
151
 
152
- if _full_word_count >= 3 and not _full_already_has_terminal and not _full_has_ellipsis:
153
  # ── FIX-29: Exclamation mark guard ──
154
  # PuncAra sometimes adds ! to declarative sentences.
155
  # Only allow ! if text contains exclamatory cues.
@@ -161,7 +161,7 @@ def validate_punctuation_diff(diff: dict, full_text: str = '') -> bool:
161
  _has_cue = any(w in _EXCL_CUES for w in full_text.split())
162
  if not _has_cue:
163
  logger.info(
164
- f"[PUNC-SAFETY] Blocked !/? on declarative sentence: "
165
  f"'{original}' → '{correction}'"
166
  )
167
  return False
@@ -175,7 +175,7 @@ def validate_punctuation_diff(diff: dict, full_text: str = '') -> bool:
175
  else:
176
  # Short fragment OR already has terminal punct → REJECT
177
  logger.info(
178
- f"[PUNC-SAFETY] Rejected terminal punct injection: "
179
  f"'{original}' → '{correction}'"
180
  )
181
  return False
 
149
  # Also check for ellipsis (... at end)
150
  _full_has_ellipsis = full_text.rstrip().endswith('...') if full_text else False
151
 
152
+ if _full_word_count >= 5 and not _full_already_has_terminal and not _full_has_ellipsis:
153
  # ── FIX-29: Exclamation mark guard ──
154
  # PuncAra sometimes adds ! to declarative sentences.
155
  # Only allow ! if text contains exclamatory cues.
 
161
  _has_cue = any(w in _EXCL_CUES for w in full_text.split())
162
  if not _has_cue:
163
  logger.info(
164
+ f"[PUNC-SAFETY] Blocked !/? on declarative sentence: "
165
  f"'{original}' → '{correction}'"
166
  )
167
  return False
 
175
  else:
176
  # Short fragment OR already has terminal punct → REJECT
177
  logger.info(
178
+ f"[PUNC-SAFETY] TerminalPunctuationGuard triggered: removing trailing punctuation "
179
  f"'{original}' → '{correction}'"
180
  )
181
  return False
src/nlp/spelling/araspell_service.py CHANGED
@@ -76,8 +76,8 @@ def get_spelling_model():
76
 
77
  epoch = checkpoint.get('epoch', 'N/A')
78
  logger.info(f"Spelling model loaded on {device}, epoch: {epoch}")
79
-
80
  # 6. Initialize the spell checker pipeline (contextual=True for MLM-based refinement)
 
81
  from nlp.spelling.araspell_rules import ArabicSpellChecker
82
  _spell_checker = ArabicSpellChecker(
83
  model, tokenizer, device, use_contextual=True
 
76
 
77
  epoch = checkpoint.get('epoch', 'N/A')
78
  logger.info(f"Spelling model loaded on {device}, epoch: {epoch}")
 
79
  # 6. Initialize the spell checker pipeline (contextual=True for MLM-based refinement)
80
+
81
  from nlp.spelling.araspell_rules import ArabicSpellChecker
82
  _spell_checker = ArabicSpellChecker(
83
  model, tokenizer, device, use_contextual=True
test_grammar_fixes.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ sys.stdout.reconfigure(encoding='utf-8')
3
+ from src.app import init_pipeline
4
+
5
+ p = init_pipeline()
6
+ tests = [
7
+ 'الأولاد لعب في الحديقة',
8
+ 'العمال بنى المبنى',
9
+ 'الطالبة متفوق في دراسته',
10
+ 'رأيت أخوك في المسجد',
11
+ 'هاتان الطالبان مجتهدان',
12
+ 'لم يفعلون الواجب بعد'
13
+ ]
14
+
15
+ with open('test_grammar_output.md', 'w', encoding='utf-8') as f:
16
+ for t in tests:
17
+ res = p.analyze(t)['corrected']
18
+ f.write(f"IN: {t}\nOUT: {res}\n---\n")
test_models.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ from pathlib import Path
4
+ sys.path.insert(0, str(Path(r'c:\Users\dell\PycharmProjects\JupyterProject1\PythonProject\BAYAN\src')))
5
+
6
+ from src.nlp.spelling.araspell_service import get_spelling_model
7
+ from src.nlp.grammar.grammar_service import get_grammar_model
8
+
9
+ print("Loading Spelling...")
10
+ spell = get_spelling_model()
11
+ print("Loading Grammar...")
12
+ grammar = get_grammar_model()
13
+
14
+ text1 = "لم ينمو الاقتصاد كالمعتاد"
15
+ text2 = "ذهبت المهندسون"
16
+
17
+ print(f"\n--- Text 1: {text1} ---")
18
+ print("AraSpell output:", spell.correct(text1))
19
+ print("Grammar output:", grammar.correct(text1))
20
+
21
+ print(f"\n--- Text 2: {text2} ---")
22
+ print("AraSpell output:", spell.correct(text2))
23
+ print("Grammar output:", grammar.correct(text2))
24
+
test_punctuation.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ sys.stdout.reconfigure(encoding='utf-8')
3
+
4
+ from src.app import init_pipeline
5
+
6
+ p = init_pipeline()
7
+
8
+ tests = [
9
+ "الخطة السنوية للشركة", # 3 words -> should block trailing .
10
+ "هذا هو تقرير المبيعات", # 4 words -> should block trailing .
11
+ "محمد ذهب إلى المدرسة اليوم", # 5 words -> allowed!
12
+ ]
13
+
14
+ with open('test_punct_output.md', 'w', encoding='utf-8') as f:
15
+ for t in tests:
16
+ res = p.analyze(t)['corrected']
17
+ f.write(f"IN: {t}\nOUT: {res}\n---\n")
tests/phase10/benchmark_runner.py CHANGED
@@ -9,6 +9,7 @@ Usage:
9
  python tests/phase10/benchmark_runner.py [--url URL] [--dataset NAMES] [--out DIR]
10
  """
11
  import argparse, json, time, re, os, sys
 
12
  from pathlib import Path
13
  from dataclasses import dataclass, field, asdict
14
  from typing import List, Dict, Optional, Any
 
9
  python tests/phase10/benchmark_runner.py [--url URL] [--dataset NAMES] [--out DIR]
10
  """
11
  import argparse, json, time, re, os, sys
12
+ sys.stdout.reconfigure(encoding='utf-8')
13
  from pathlib import Path
14
  from dataclasses import dataclass, field, asdict
15
  from typing import List, Dict, Optional, Any
tests/phase10/reports/phase10_results.json CHANGED
The diff for this file is too large to render. See raw diff