youssefreda9 commited on
Commit
e658303
·
1 Parent(s): 01b11d4

revert: restore backend to 1a76471, keep all UI/UX improvements

Browse files
.github/workflows/deploy.yml CHANGED
@@ -7,30 +7,8 @@ on:
7
  branches: [main]
8
 
9
  jobs:
10
- test:
11
- name: Run Regression Tests
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v4
15
-
16
- - name: Setup Python
17
- uses: actions/setup-python@v5
18
- with:
19
- python-version: '3.12'
20
- cache: 'pip'
21
-
22
- - name: Install dependencies
23
- run: |
24
- pip install flask flask-cors python-dotenv pytest
25
- pip install -r requirements.txt || true
26
-
27
- - name: Run pipeline tests
28
- run: |
29
- cd src && python -m pytest ../tests/test_pipeline_hardening.py ../tests/test_bug_fixes.py -v --tb=short
30
-
31
  lint-and-validate:
32
  name: Validate Code
33
- needs: test
34
  runs-on: ubuntu-latest
35
  steps:
36
  - uses: actions/checkout@v4
 
7
  branches: [main]
8
 
9
  jobs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  lint-and-validate:
11
  name: Validate Code
 
12
  runs-on: ubuntu-latest
13
  steps:
14
  - uses: actions/checkout@v4
hybrid_module.py DELETED
@@ -1,147 +0,0 @@
1
-
2
- # hybrid_module.py
3
-
4
- import torch
5
- import pickle
6
- from transformers import GPT2LMHeadModel, GPT2Tokenizer
7
- from huggingface_hub import hf_hub_download
8
-
9
- # ---------- Load Bigram ----------
10
-
11
- def load_bigram(repo_id="bayan10/AutoComplete", filename="bigram_model_v4.pkl"):
12
- path = hf_hub_download(repo_id=repo_id, filename=filename)
13
- with open(path, "rb") as f:
14
- data = pickle.load(f)
15
- return data["unigrams"], data["bigrams"]
16
-
17
- # ---------- Load GPT-2 ----------
18
- def load_gpt2(model_name="aubmindlab/aragpt2-base"):
19
- tokenizer = GPT2Tokenizer.from_pretrained(model_name)
20
- model = GPT2LMHeadModel.from_pretrained(model_name)
21
- tokenizer.pad_token = tokenizer.eos_token
22
- model.config.pad_token_id = tokenizer.eos_token_id
23
- model.eval()
24
- return tokenizer, model
25
-
26
- # ---------- GPT-2 scoring ----------
27
- def gpt2_next_token_probs(prefix, tokenizer, model, top_k=50):
28
- inputs = tokenizer(
29
- prefix,
30
- return_tensors="pt",
31
- truncation=True,
32
- max_length=1024
33
- )
34
-
35
- with torch.no_grad():
36
- outputs = model(**inputs)
37
- logits = outputs.logits[0, -1]
38
-
39
- probs = torch.softmax(logits, dim=-1)
40
- top_probs, top_ids = torch.topk(probs, top_k)
41
-
42
- prob_dict = {}
43
- for idx, prob in zip(top_ids, top_probs):
44
- word = tokenizer.decode([idx]).strip()
45
- if word:
46
- prob_dict[word] = prob.item()
47
-
48
- return prob_dict
49
-
50
- # ---------- Statistical autocomplete ----------
51
-
52
-
53
- def statistical_autocomplete(text, unigrams, bigrams, top_k=20):
54
- tokens = text.strip().split()
55
- if not tokens:
56
- return []
57
-
58
- last_word = tokens[-1]
59
- candidates = []
60
-
61
- if last_word in bigrams:
62
- for w, c in bigrams[last_word].items():
63
- if len(w) < 3 or w == last_word:
64
- continue
65
- candidates.append((w, c))
66
-
67
- if not candidates:
68
- for w, c in unigrams.items():
69
- if len(w) < 3:
70
- continue
71
- candidates.append((w, c))
72
-
73
- total = sum(c for _, c in candidates)
74
- preds = [(w, c / total) for w, c in candidates]
75
- preds.sort(key=lambda x: x[1], reverse=True)
76
- preds = merge_similar_predictions(preds, top_k=top_k)
77
- return preds[:top_k]
78
-
79
- # ---------- Hybrid autocomplete ----------
80
- def hybrid_autocomplete(prefix, unigrams, bigrams, tokenizer, model, alpha=0.6, k=5):
81
- words = prefix.strip().split()
82
- if len(words) < 1:
83
- return []
84
-
85
- last_word = words[-1]
86
- if last_word not in bigrams:
87
- return []
88
-
89
- # -------- Statistical (Bigram) --------
90
- stat_candidates = statistical_autocomplete(
91
- prefix,
92
- unigrams,
93
- bigrams,
94
- top_k=20
95
- )
96
-
97
- # -------- Neural (GPT-2) — ONCE --------
98
- gpt2_probs = gpt2_next_token_probs(prefix, tokenizer, model, top_k=50)
99
-
100
- # -------- Hybrid scoring --------
101
- results = []
102
- for w, stat_p in stat_candidates:
103
- neural_p = gpt2_probs.get(w, 1e-8) # small value if not found
104
- score = alpha * stat_p + (1 - alpha) * neural_p
105
- results.append((w, score))
106
-
107
- return sorted(results, key=lambda x: x[1], reverse=True)[:k]
108
-
109
- import re
110
- from collections import defaultdict
111
-
112
- def canonical_form(word):
113
- word = re.sub("[إأآا]", "ا", word)
114
- word = re.sub("ى", "ي", word)
115
- word = re.sub("ؤ", "و", word)
116
- word = re.sub("ئ", "ي", word)
117
- word = re.sub("ة", "ه", word)
118
- word = re.sub(r"[ًٌٍَُِّْ]", "", word)
119
- return word
120
-
121
-
122
-
123
- def merge_similar_predictions(preds, top_k=20):
124
- groups = defaultdict(lambda: {"score": 0.0, "words": []})
125
-
126
- for w, p in preds:
127
- key = canonical_form(w)
128
- groups[key]["score"] += p
129
- groups[key]["words"].append(w)
130
-
131
- merged = sorted(
132
- groups.values(),
133
- key=lambda x: x["score"],
134
- reverse=True
135
- )
136
-
137
- return [
138
- (group["words"][0], group["score"])
139
- for group in merged[:top_k]
140
- ]
141
-
142
-
143
-
144
-
145
-
146
-
147
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app.py CHANGED
@@ -831,14 +831,6 @@ def _is_small_spelling_change(orig_word, corr_word, vocab_manager=None):
831
  if corr_word == orig_word[:-1] or len(corr_word) < len(orig_word):
832
  return 0.0
833
 
834
- # ── GUARD 3: Word truncation protection (Phase 4, P3) ──
835
- # Reject corrections that significantly shorten a valid word.
836
- # E.g. الدمث→الدم (rare word truncated to different word).
837
- if vocab_manager and len(orig_word) - len(corr_word) >= 2:
838
- if vocab_manager.is_iv(orig_word):
839
- logger.info(f"[SPELLING] Blocked truncation: '{orig_word}'→'{corr_word}' (valid word shortened by {len(orig_word) - len(corr_word)} chars)")
840
- return 0.0
841
-
842
  # CRITICAL: If both words are valid Arabic words, only accept known fixes.
843
  # This prevents the spelling model from changing one correct word to another
844
  # (e.g. وكان→وكأن, which changes "and was" to "as if" — a meaning change).
@@ -1338,26 +1330,6 @@ def analyze_text():
1338
  logger.error(traceback.format_exc())
1339
  timing_ms['spelling_error'] = f"{type(e).__name__}: {str(e)[:200]}"
1340
 
1341
- # ── Standalone hamza-fix pass (P1/P2) ──
1342
- # fix_common_hamza handles common hamza errors that AraSpell missed
1343
- # in sentence context: انت→أنت, الان→الآن, ام→أم, انا→أنا, etc.
1344
- # This runs on the FULL text regardless of whether AraSpell ran.
1345
- try:
1346
- from nlp.spelling.araspell_rules import AraSpellPostProcessor
1347
- hamza_fixed = AraSpellPostProcessor.fix_common_hamza(current_text)
1348
- if hamza_fixed != current_text:
1349
- hamza_diffs = get_word_diffs(current_text, hamza_fixed)
1350
- for hd in hamza_diffs:
1351
- ctx.add_patch(
1352
- 'spelling', hd['start'], hd['end'],
1353
- hd['correction'], confidence=0.95,
1354
- )
1355
- logger.info(f"[HAMZA-FIX] '{hd.get('original','')}' → '{hd.get('correction','')}'")
1356
- ctx.mutate_text(hamza_fixed, OffsetMapper)
1357
- current_text = ctx.current_text
1358
- except Exception as e:
1359
- logger.error(f"[ANALYZE] Hamza fix failed: {type(e).__name__}: {e}")
1360
-
1361
  # 2. Grammar (runs on spelling-corrected text — word-level dependency)
1362
  try:
1363
  t0 = time.time()
@@ -1369,40 +1341,7 @@ def analyze_text():
1369
  logger.info(f"[ANALYZE] Step 2: Grammar done in {timing_ms['grammar_ms']}ms")
1370
  if corrected_grammar != ctx.current_text:
1371
  diffs = get_word_diffs(ctx.current_text, corrected_grammar)
1372
-
1373
- # ── Phase 2 (P1): Split multi-word grammar diffs ──
1374
- # Break merged diffs like "الي المدرسه الاستاذ"→"إلى المدرسة الأستاذ"
1375
- # into individual word-level suggestions.
1376
- expanded_diffs = []
1377
  for d in diffs:
1378
- orig_text = d.get('original', '')
1379
- corr_text = d.get('correction', '')
1380
- orig_words_list = orig_text.split()
1381
- corr_words_list = corr_text.split()
1382
- # Only split if same word count and multiple words
1383
- if (len(orig_words_list) > 1
1384
- and len(orig_words_list) == len(corr_words_list)):
1385
- # Split into per-word diffs
1386
- search_from = d['start']
1387
- for ow, cw in zip(orig_words_list, corr_words_list):
1388
- if ow != cw:
1389
- w_start = ctx.current_text.find(ow, search_from)
1390
- if w_start >= 0:
1391
- w_end = w_start + len(ow)
1392
- expanded_diffs.append({
1393
- 'start': w_start, 'end': w_end,
1394
- 'original': ow, 'correction': cw,
1395
- 'type': 'generic'
1396
- })
1397
- search_from = w_end
1398
- else:
1399
- search_from += len(ow) + 1
1400
- else:
1401
- search_from += len(ow) + 1
1402
- else:
1403
- expanded_diffs.append(d)
1404
-
1405
- for d in expanded_diffs:
1406
  # StageLocker: skip diffs that overlap with locked ranges
1407
  if ctx.stage_locker.is_locked(d['start'], d['end']):
1408
  logger.info(
@@ -1411,25 +1350,6 @@ def analyze_text():
1411
  )
1412
  continue
1413
 
1414
- # ── Punctuation-protection guard ──
1415
- # Grammar model sometimes strips/normalizes punctuation
1416
- # (removes full stops, commas, etc.). Reject grammar diffs
1417
- # that only differ in punctuation marks — grammar should
1418
- # only fix grammar, not touch punctuation.
1419
- _orig_t = d.get('original', '')
1420
- _corr_t = d.get('correction', '')
1421
- _PUNC_CHARS = set('.,;:!?؟،؛。·…•–—\u200f\u200e')
1422
- _orig_no_punc = ''.join(c for c in _orig_t if c not in _PUNC_CHARS)
1423
- _corr_no_punc = ''.join(c for c in _corr_t if c not in _PUNC_CHARS)
1424
- if _orig_no_punc == _corr_no_punc:
1425
- # Only punctuation was changed — block it
1426
- if _orig_t != _corr_t:
1427
- logger.info(
1428
- f"[GRAMMAR] Blocked punctuation change: "
1429
- f"'{_orig_t}' → '{_corr_t}' (grammar must not alter punctuation)"
1430
- )
1431
- continue
1432
-
1433
  # Reject grammar hallucinations (e.g. جالس→جاكسون)
1434
  orig_text = d.get('original', '')
1435
  corr_text = d.get('correction', '')
 
831
  if corr_word == orig_word[:-1] or len(corr_word) < len(orig_word):
832
  return 0.0
833
 
 
 
 
 
 
 
 
 
834
  # CRITICAL: If both words are valid Arabic words, only accept known fixes.
835
  # This prevents the spelling model from changing one correct word to another
836
  # (e.g. وكان→وكأن, which changes "and was" to "as if" — a meaning change).
 
1330
  logger.error(traceback.format_exc())
1331
  timing_ms['spelling_error'] = f"{type(e).__name__}: {str(e)[:200]}"
1332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1333
  # 2. Grammar (runs on spelling-corrected text — word-level dependency)
1334
  try:
1335
  t0 = time.time()
 
1341
  logger.info(f"[ANALYZE] Step 2: Grammar done in {timing_ms['grammar_ms']}ms")
1342
  if corrected_grammar != ctx.current_text:
1343
  diffs = get_word_diffs(ctx.current_text, corrected_grammar)
 
 
 
 
 
1344
  for d in diffs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1345
  # StageLocker: skip diffs that overlap with locked ranges
1346
  if ctx.stage_locker.is_locked(d['start'], d['end']):
1347
  logger.info(
 
1350
  )
1351
  continue
1352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1353
  # Reject grammar hallucinations (e.g. جالس→جاكسون)
1354
  orig_text = d.get('original', '')
1355
  corr_text = d.get('correction', '')
trace_output.txt DELETED
File without changes
trace_punc.py DELETED
@@ -1,73 +0,0 @@
1
- import sys, os, re
2
- sys.path.insert(0, 'src')
3
- import logging; logging.basicConfig(level=logging.INFO)
4
- print("Starting...")
5
-
6
- import torch
7
- print(f"CUDA available: {torch.cuda.is_available()}")
8
-
9
- from transformers import EncoderDecoderModel, AutoTokenizer
10
- print("Loading PuncAra-v1...")
11
- model = EncoderDecoderModel.from_pretrained("bayan10/PuncAra-v1")
12
- tokenizer = AutoTokenizer.from_pretrained("bayan10/PuncAra-v1")
13
- model.eval()
14
- print("Model loaded!")
15
-
16
- inp = "التزم الرياضي بتناول وجباته الصحية وحساب سعراته بدقة رغبة في بناء كتلة عضلية قوية ويا له من التزام حديدي يثير الإعجاب"
17
- print(f"\nINPUT: {inp}")
18
-
19
- # Raw inference
20
- from nlp.punctuation.punctuation_rules import arabic_preprocessing
21
- processed = arabic_preprocessing(inp)
22
- inputs = tokenizer(processed, return_tensors="pt", padding=True, truncation=True, max_length=128)
23
- print("Running inference...")
24
- with torch.no_grad():
25
- outputs = model.generate(
26
- inputs.input_ids,
27
- attention_mask=inputs.attention_mask,
28
- decoder_start_token_id=tokenizer.cls_token_id,
29
- bos_token_id=tokenizer.cls_token_id,
30
- eos_token_id=tokenizer.sep_token_id,
31
- pad_token_id=tokenizer.pad_token_id,
32
- max_length=128, num_beams=3, repetition_penalty=1.2,
33
- length_penalty=1.0, early_stopping=True, do_sample=False
34
- )
35
- raw = tokenizer.decode(outputs[0], skip_special_tokens=True)
36
- print(f"[A] RAW MODEL: {raw}")
37
-
38
- # Strip non-punc
39
- from nlp.punctuation.punctuation_service import PunctuationChecker
40
- checker = PunctuationChecker(model, tokenizer, torch.device('cpu'))
41
- stripped = checker._strip_non_punctuation_changes(inp, raw)
42
- print(f"[B] STRIPPED: {stripped}")
43
- if stripped != raw:
44
- rw, sw = raw.split(), stripped.split()
45
- for w1, w2 in zip(rw, sw):
46
- if w1 != w2:
47
- print(f" LOST: '{w1}' -> '{w2}'")
48
-
49
- # Postprocess
50
- from nlp.punctuation.punctuation_rules import arabic_postprocessing
51
- final = arabic_postprocessing(stripped)
52
- print(f"[C] FINAL: {final}")
53
-
54
- # Diffs
55
- from app import get_word_diffs
56
- from nlp.punctuation.punctuation_rules import validate_punctuation_diff
57
- if final != inp:
58
- diffs = get_word_diffs(inp, final)
59
- print(f"[D] DIFFS ({len(diffs)}):")
60
- for d in diffs:
61
- o, c = d.get('original',''), d.get('correction','')
62
- valid = validate_punctuation_diff(d)
63
- oa = re.sub(r'[^\u0600-\u06FFa-zA-Z]','',o)
64
- ca = re.sub(r'[^\u0600-\u06FFa-zA-Z]','',c)
65
- alpha_ok = oa == ca
66
- s = "PASS" if valid and alpha_ok else "BLOCKED"
67
- r = ""
68
- if not valid: r += " safety"
69
- if not alpha_ok: r += " alpha"
70
- print(f" [{d['start']}:{d['end']}] '{o}' -> '{c}' [{s}{r}]")
71
- else:
72
- print("[D] NO DIFFS!")
73
- print("\nDONE")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
trace_punctuation.py DELETED
@@ -1,176 +0,0 @@
1
- """
2
- BAYAN Punctuation Trace — Diagnose where punctuation marks get lost.
3
-
4
- Compares:
5
- A) Raw PuncAra model output (no pipeline)
6
- B) After _strip_non_punctuation_changes (Fix P1)
7
- C) After get_word_diffs (diff algorithm)
8
- D) After StageLocker check
9
- E) After validate_punctuation_diff (safety layer)
10
- F) After overlap resolver + patch cap
11
- """
12
-
13
- import sys, os, re, difflib
14
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
15
-
16
- # Suppress model loading noise
17
- import logging
18
- logging.basicConfig(level=logging.WARNING)
19
-
20
- # ─── Test Sentences ─────────────────────────────────────────────
21
- TEST_SENTENCES = [
22
- {
23
- "input": "التزم الرياضي بتناول وجباته الصحية وحساب سعراته بدقة رغبة في بناء كتلة عضلية قوية ويا له من التزام حديدي يثير الإعجاب",
24
- "expected": "التزم الرياضي بتناول وجباته الصحية وحساب سعراته بدقة؛ رغبة في بناء كتلة عضلية قوية، ويا له من التزام حديدي يثير الإعجاب!",
25
- },
26
- {
27
- "input": "كانت الفتيات يلعبن في الحديقة وفجأة سقطت إحداهن وبدأت تبكي بشدة",
28
- "expected": "كانت الفتيات يلعبن في الحديقة، وفجأة سقطت إحداهن وبدأت تبكي بشدة.",
29
- },
30
- {
31
- "input": "إن الذكاء الاصطناعي يلعب دورا هاما لذلك يجب الاهتمام به",
32
- "expected": "إن الذكاء الاصطناعي يلعب دورا هاما؛ لذلك يجب الاهتمام به.",
33
- },
34
- {
35
- "input": "هل تعلم أن القاهرة هي عاصمة مصر وتقع على ضفاف نهر النيل",
36
- "expected": "هل تعلم أن القاهرة هي عاصمة مصر، وتقع على ضفاف نهر النيل؟",
37
- },
38
- {
39
- "input": "قال المعلم للطلاب ادرسوا جيدا فالامتحان قريب",
40
- "expected": "قال المعلم للطلاب: ادرسوا جيدا، فالامتحان قريب.",
41
- },
42
- ]
43
-
44
- def count_punct(text):
45
- """Count punctuation marks in text."""
46
- marks = set('.,;:!?،؛؟')
47
- return sum(1 for c in text if c in marks)
48
-
49
- def diff_punct(before, after):
50
- """Show what punctuation marks were added/removed."""
51
- marks = set('.,;:!?،؛؟')
52
- before_marks = [(i, c) for i, c in enumerate(before) if c in marks]
53
- after_marks = [(i, c) for i, c in enumerate(after) if c in marks]
54
- return before_marks, after_marks
55
-
56
- def main():
57
- print("=" * 80)
58
- print("BAYAN PUNCTUATION TRACE — Where do punctuation marks get lost?")
59
- print("=" * 80)
60
-
61
- # Load model
62
- print("\n[1/2] Loading PuncAra-v1 model...")
63
- from nlp.punctuation.punctuation_service import get_punctuation_model, PunctuationChecker
64
- punc_checker = get_punctuation_model()
65
- print(" ✓ Model loaded\n")
66
-
67
- # Load pipeline tools
68
- print("[2/2] Loading pipeline tools...")
69
- from app import get_word_diffs
70
- from nlp.punctuation.punctuation_rules import validate_punctuation_diff
71
- print(" ✓ Tools loaded\n")
72
-
73
- for idx, test in enumerate(TEST_SENTENCES):
74
- inp = test["input"]
75
- expected = test["expected"]
76
-
77
- print("─" * 80)
78
- print(f"TEST {idx+1}")
79
- print(f" INPUT: {inp}")
80
- print(f" EXPECTED: {expected}")
81
- print(f" Expected marks: {count_punct(expected)}")
82
- print()
83
-
84
- # ─── Stage A: Raw model output (no post-processing) ────────
85
- raw_output = punc_checker._fix_punctuation(inp)
86
- print(f" [A] RAW MODEL: {raw_output}")
87
- print(f" Marks added: {count_punct(raw_output) - count_punct(inp)}")
88
- print()
89
-
90
- # ─── Stage B: After _strip_non_punctuation_changes ─────────
91
- stripped = punc_checker._strip_non_punctuation_changes(inp, raw_output)
92
- print(f" [B] STRIP NON-PUNC: {stripped}")
93
- if stripped != raw_output:
94
- print(f" ⚠ Changes stripped! Diff from raw:")
95
- for w1, w2 in zip(raw_output.split(), stripped.split()):
96
- if w1 != w2:
97
- print(f" '{w1}' → '{w2}'")
98
- print(f" Marks added: {count_punct(stripped) - count_punct(inp)}")
99
- print()
100
-
101
- # ─── Stage C: get_word_diffs ───────────────────────────────
102
- # This is what correct() returns after postprocessing
103
- from nlp.punctuation.punctuation_rules import arabic_postprocessing
104
- final_punc = arabic_postprocessing(stripped)
105
-
106
- print(f" [C] FINAL PUNC OUT: {final_punc}")
107
- print(f" Marks added: {count_punct(final_punc) - count_punct(inp)}")
108
- print()
109
-
110
- # ─── Stage D: Word diffs ──────────────────────────────────
111
- if final_punc != inp:
112
- diffs = get_word_diffs(inp, final_punc)
113
- print(f" [D] WORD DIFFS ({len(diffs)} found):")
114
- for d in diffs:
115
- orig = d.get('original', '')
116
- corr = d.get('correction', '')
117
-
118
- # Check validate_punctuation_diff
119
- is_valid = validate_punctuation_diff(d)
120
-
121
- # Check alpha match (lock bypass)
122
- orig_alpha = re.sub(r'[^\u0600-\u06FFa-zA-Z]', '', orig)
123
- corr_alpha = re.sub(r'[^\u0600-\u06FFa-zA-Z]', '', corr)
124
- alpha_match = orig_alpha == corr_alpha
125
-
126
- status_parts = []
127
- if not is_valid:
128
- status_parts.append("❌ SAFETY-REJECTED")
129
- if not alpha_match:
130
- status_parts.append("❌ LOCK-BLOCKED (alpha differs)")
131
- if is_valid and alpha_match:
132
- status_parts.append("✅ WOULD PASS")
133
- elif is_valid:
134
- status_parts.append("✅ valid-punc")
135
-
136
- status = " | ".join(status_parts)
137
- print(f" [{d['start']}:{d['end']}] '{orig}' → '{corr}' {status}")
138
- else:
139
- print(f" [D] NO DIFFS — model returned same text as input!")
140
-
141
- print()
142
-
143
- # ─── Summary ───────────────────────────────────────────────────
144
- print("=" * 80)
145
- print("LOSS POINTS SUMMARY")
146
- print("=" * 80)
147
- print("""
148
- Where punctuation marks can be lost:
149
-
150
- [A→B] _strip_non_punctuation_changes():
151
- If model changes a word's spelling AND adds punctuation,
152
- the punctuation transfer logic may fail.
153
-
154
- [B→C] arabic_postprocessing():
155
- Typographic cleanup may remove valid marks.
156
-
157
- [C→D] get_word_diffs():
158
- Word-level diff may merge/split changes incorrectly.
159
-
160
- [D→E] StageLocker:
161
- Locked ranges from spelling/grammar block nearby punctuation.
162
- (Now relaxed: pure-punc changes pass through)
163
-
164
- [D→E] validate_punctuation_diff():
165
- Safety layer rejects diffs that change Arabic text.
166
-
167
- [E→F] Overlap resolver:
168
- Grammar/spelling patches take priority over punctuation.
169
-
170
- [E→F] Patch cap:
171
- Max 3 punctuation patches per response.
172
- """)
173
-
174
-
175
- if __name__ == "__main__":
176
- main()