youssefreda9 commited on
Commit
7f0ddc6
·
1 Parent(s): b0bed28

fix: Remove 'Apply All' button - conflicts with spelling+grammar pipeline

Browse files
src/app.py CHANGED
@@ -11,7 +11,6 @@ from flask_cors import CORS
11
  from pathlib import Path
12
  import traceback
13
  import difflib
14
- from difflib import SequenceMatcher
15
  import re
16
 
17
  # Load .env file from project root (one level up from src/)
@@ -682,8 +681,6 @@ def analyze_text():
682
 
683
  current_text = text
684
  suggestions = []
685
- spelling_changed_originals = set()
686
- spell_checker = None
687
  mappers = []
688
  total_start = time.time()
689
 
@@ -846,51 +843,13 @@ def analyze_text():
846
  continue
847
 
848
  safe_text = " ".join(new_words)
849
- # Track which original words were changed by spelling
850
- for s in suggestions:
851
- spelling_changed_originals.add(s['original'])
852
  current_text = safe_text
853
  except Exception as e:
854
  logger.error(f"[ANALYZE] Spelling failed: {e}")
855
 
856
- # Build word position map for original text (needed for grammar mapping)
857
- orig_word_positions = get_word_positions(text)
858
-
859
- # Build mapping: corrected_text word index → original_text word index
860
- # This handles word splits (1 orig word → N corrected words)
861
- spelling_word_map = [] # spelling_word_map[j] = original word index
862
- orig_words_list = text.split()
863
- corrected_words_list = current_text.split()
864
-
865
- if current_text != text:
866
- sm_map = SequenceMatcher(None, orig_words_list, corrected_words_list)
867
- for tag, i1, i2, j1, j2 in sm_map.get_opcodes():
868
- if tag == 'equal':
869
- for k in range(j1, j2):
870
- spelling_word_map.append(i1 + (k - j1))
871
- elif tag == 'replace':
872
- # Map corrected words proportionally to original words
873
- o_count = i2 - i1
874
- c_count = j2 - j1
875
- for k in range(j1, j2):
876
- # Map proportionally
877
- orig_idx = i1 + min(int((k - j1) * o_count / c_count), o_count - 1)
878
- spelling_word_map.append(orig_idx)
879
- elif tag == 'insert':
880
- for k in range(j1, j2):
881
- # Inserted words map to the nearest original word
882
- spelling_word_map.append(min(i1, len(orig_words_list) - 1))
883
- elif tag == 'delete':
884
- pass # deleted original words have no corrected counterpart
885
- else:
886
- # No spelling changes — 1:1 mapping
887
- spelling_word_map = list(range(len(orig_words_list)))
888
-
889
- # Save text after spelling for grammar diff
890
- text_after_spelling = current_text
891
-
892
  # 2. Grammar (runs on spelling-corrected text)
893
- has_grammar = True
894
  if has_grammar:
895
  try:
896
  t0 = time.time()
@@ -899,104 +858,17 @@ def analyze_text():
899
  corrected_grammar = correct_grammar(current_text)
900
  logger.info(f"[ANALYZE] Step 2: Grammar done in {time.time()-t0:.2f}s")
901
  if corrected_grammar != current_text:
902
- # Word-by-word diff: spelling-corrected vs grammar-corrected
903
- spell_words = current_text.split()
904
- grammar_words = corrected_grammar.split()
905
-
906
- # Existing spelling suggestion positions (for overlap check)
907
- spelling_ranges = [(s['start'], s['end']) for s in suggestions]
908
-
909
- sm_grammar = SequenceMatcher(None, spell_words, grammar_words)
910
- for tag, i1, i2, j1, j2 in sm_grammar.get_opcodes():
911
- if tag != 'replace':
912
- continue
913
-
914
- o_count = i2 - i1
915
- c_count = j2 - j1
916
-
917
- # If word counts match, process EACH word individually
918
- if o_count == c_count:
919
- for offset in range(o_count):
920
- sw = spell_words[i1 + offset]
921
- gw = grammar_words[j1 + offset]
922
- if sw == gw:
923
- continue # no change at this position
924
-
925
- # Map to original text position
926
- spell_idx = i1 + offset
927
- if spell_idx >= len(spelling_word_map):
928
- continue
929
- orig_idx = spelling_word_map[spell_idx]
930
- if orig_idx >= len(orig_word_positions):
931
- continue
932
-
933
- orig_start = orig_word_positions[orig_idx][1]
934
- orig_end = orig_word_positions[orig_idx][2]
935
- original_word = text[orig_start:orig_end]
936
-
937
- if original_word == gw:
938
- continue
939
-
940
- # Overlap check
941
- overlaps = any(
942
- not (orig_end <= sr_s or orig_start >= sr_e)
943
- for sr_s, sr_e in spelling_ranges
944
- )
945
- if overlaps:
946
- continue
947
-
948
- suggestions.append({
949
- 'start': orig_start,
950
- 'end': orig_end,
951
- 'original': original_word,
952
- 'correction': gw,
953
- 'type': 'grammar'
954
- })
955
- else:
956
- # Word count differs — create one grouped suggestion
957
- # but only if it's a small block (max 3 words)
958
- if o_count > 3 or c_count > 3:
959
- continue # skip large blocks (likely hallucination)
960
-
961
- g_segment = grammar_words[j1:j2]
962
- correction_text = " ".join(g_segment)
963
-
964
- orig_indices = set()
965
- for k in range(i1, i2):
966
- if k < len(spelling_word_map):
967
- orig_indices.add(spelling_word_map[k])
968
-
969
- if not orig_indices:
970
- continue
971
-
972
- min_orig = min(orig_indices)
973
- max_orig = max(orig_indices)
974
-
975
- if min_orig >= len(orig_word_positions) or max_orig >= len(orig_word_positions):
976
- continue
977
-
978
- orig_start = orig_word_positions[min_orig][1]
979
- orig_end = orig_word_positions[max_orig][2]
980
- original_span = text[orig_start:orig_end]
981
-
982
- if original_span == correction_text:
983
- continue
984
-
985
- overlaps = any(
986
- not (orig_end <= sr_s or orig_start >= sr_e)
987
- for sr_s, sr_e in spelling_ranges
988
- )
989
- if overlaps:
990
- continue
991
-
992
- suggestions.append({
993
- 'start': orig_start,
994
- 'end': orig_end,
995
- 'original': original_span,
996
- 'correction': correction_text,
997
- 'type': 'grammar'
998
- })
999
-
1000
  current_text = corrected_grammar
1001
  except Exception as e:
1002
  logger.error(f"[ANALYZE] Grammar failed: {e}")
@@ -1013,6 +885,16 @@ def analyze_text():
1013
  corrected_punc = punctuation_model.add_punctuation(current_text)
1014
  logger.info(f"[ANALYZE] Step 3: Punctuation done in {time.time()-t0:.2f}s")
1015
  if corrected_punc != current_text:
 
 
 
 
 
 
 
 
 
 
1016
  current_text = corrected_punc
1017
  except Exception as e:
1018
  logger.error(f"[ANALYZE] Punctuation failed: {e}")
 
11
  from pathlib import Path
12
  import traceback
13
  import difflib
 
14
  import re
15
 
16
  # Load .env file from project root (one level up from src/)
 
681
 
682
  current_text = text
683
  suggestions = []
 
 
684
  mappers = []
685
  total_start = time.time()
686
 
 
843
  continue
844
 
845
  safe_text = " ".join(new_words)
846
+ mappers.append(OffsetMapper(current_text, safe_text))
 
 
847
  current_text = safe_text
848
  except Exception as e:
849
  logger.error(f"[ANALYZE] Spelling failed: {e}")
850
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
851
  # 2. Grammar (runs on spelling-corrected text)
852
+ has_grammar = True # Always available via lazy-loaded grammar_service
853
  if has_grammar:
854
  try:
855
  t0 = time.time()
 
858
  corrected_grammar = correct_grammar(current_text)
859
  logger.info(f"[ANALYZE] Step 2: Grammar done in {time.time()-t0:.2f}s")
860
  if corrected_grammar != current_text:
861
+ diffs = get_word_diffs(current_text, corrected_grammar)
862
+ for d in diffs:
863
+ orig_start, orig_end = map_range_to_original(d['start'], d['end'])
864
+ suggestions.append({
865
+ 'start': orig_start,
866
+ 'end': orig_end,
867
+ 'original': text[orig_start:orig_end],
868
+ 'correction': d['correction'],
869
+ 'type': 'grammar'
870
+ })
871
+ mappers.append(OffsetMapper(current_text, corrected_grammar))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
872
  current_text = corrected_grammar
873
  except Exception as e:
874
  logger.error(f"[ANALYZE] Grammar failed: {e}")
 
885
  corrected_punc = punctuation_model.add_punctuation(current_text)
886
  logger.info(f"[ANALYZE] Step 3: Punctuation done in {time.time()-t0:.2f}s")
887
  if corrected_punc != current_text:
888
+ diffs = get_word_diffs(current_text, corrected_punc)
889
+ for d in diffs:
890
+ orig_start, orig_end = map_range_to_original(d['start'], d['end'])
891
+ suggestions.append({
892
+ 'start': orig_start,
893
+ 'end': orig_end,
894
+ 'original': text[orig_start:orig_end],
895
+ 'correction': d['correction'],
896
+ 'type': 'punctuation'
897
+ })
898
  current_text = corrected_punc
899
  except Exception as e:
900
  logger.error(f"[ANALYZE] Punctuation failed: {e}")
src/index.html CHANGED
@@ -560,7 +560,7 @@
560
  <div class="surface-card sidebar-card">
561
  <h3 class="text-xl font-bold mb-4 text-right">الاقتراحات</h3>
562
  <div id="suggestions-list" class="suggestions-scroll" role="list" aria-live="polite" aria-label="اقتراحات التصحيح"></div>
563
- <button id="apply-all-btn" class="btn-primary apply-all-btn is-hidden" type="button">تطبيق الكل</button>
564
  </div>
565
  </div>
566
  </div>
@@ -577,7 +577,7 @@
577
  <button id="bottom-sheet-close" class="btn-ghost" type="button" aria-label="إغلاق">إغلاق</button>
578
  </div>
579
  <div id="bottom-sheet-list" class="suggestions-scroll" role="list" aria-live="polite"></div>
580
- <button id="apply-all-sheet" class="btn-primary apply-all-btn mt-4 is-hidden" type="button">تطبيق الكل</button>
581
  </div>
582
  </div>
583
  <!-- Mobile export sheet (documents) -->
 
560
  <div class="surface-card sidebar-card">
561
  <h3 class="text-xl font-bold mb-4 text-right">الاقتراحات</h3>
562
  <div id="suggestions-list" class="suggestions-scroll" role="list" aria-live="polite" aria-label="اقتراحات التصحيح"></div>
563
+ <!-- apply-all removed: conflicts with multi-step spelling+grammar pipeline -->
564
  </div>
565
  </div>
566
  </div>
 
577
  <button id="bottom-sheet-close" class="btn-ghost" type="button" aria-label="إغلاق">إغلاق</button>
578
  </div>
579
  <div id="bottom-sheet-list" class="suggestions-scroll" role="list" aria-live="polite"></div>
580
+ <!-- apply-all removed: conflicts with multi-step spelling+grammar pipeline -->
581
  </div>
582
  </div>
583
  <!-- Mobile export sheet (documents) -->
src/js/editor.js CHANGED
@@ -362,14 +362,8 @@ function applyAllSuggestions() {
362
  if (suggestions.length === 0) return;
363
 
364
  let text = getEditorText();
365
- // Track applied ranges to skip overlapping suggestions
366
- const appliedRanges = [];
367
  suggestions.forEach((s) => {
368
- // Check if this suggestion overlaps with any already-applied one
369
- const overlaps = appliedRanges.some(([rs, re]) => !(s.end <= rs || s.start >= re));
370
- if (overlaps) return; // skip overlapping
371
  text = text.substring(0, s.start) + s.correction + text.substring(s.end);
372
- appliedRanges.push([s.start, s.end]);
373
  });
374
 
375
  setEditorHTML(escapeHtml(text));
 
362
  if (suggestions.length === 0) return;
363
 
364
  let text = getEditorText();
 
 
365
  suggestions.forEach((s) => {
 
 
 
366
  text = text.substring(0, s.start) + s.correction + text.substring(s.end);
 
367
  });
368
 
369
  setEditorHTML(escapeHtml(text));
src/nlp/grammar/grammar_service.py CHANGED
@@ -102,54 +102,16 @@ def correct_grammar(text: str) -> str:
102
  # Decode output
103
  corrected = tokenizer.decode(outputs[0], skip_special_tokens=True)
104
 
105
- # === VALIDATION ===
106
-
107
- # 1. Empty check
108
  if not corrected or not corrected.strip():
109
  return text
110
 
111
- corrected = corrected.strip()
112
-
113
- # 2. Length check: too short = hallucination
114
  if len(corrected) < len(text) * 0.3:
115
  logger.warning(f"Grammar output too short ({len(corrected)} vs {len(text)}), keeping original")
116
  return text
117
 
118
- # 3. Length check: too long = repetition/hallucination
119
- if len(corrected) > len(text) * 2.0:
120
- logger.warning(f"Grammar output too long ({len(corrected)} vs {len(text)}), keeping original")
121
- return text
122
-
123
- # 4. Word count check: grammar shouldn't drastically change word count
124
- orig_wc = len(text.split())
125
- corr_wc = len(corrected.split())
126
- if orig_wc > 0:
127
- ratio = corr_wc / orig_wc
128
- if ratio < 0.5 or ratio > 2.0:
129
- logger.warning(f"Grammar word count mismatch ({corr_wc} vs {orig_wc}), keeping original")
130
- return text
131
-
132
- # 5. Repetition detection: reject if output has excessive repeated words
133
- corr_words = corrected.split()
134
- if len(corr_words) >= 4:
135
- consecutive_repeats = 0
136
- for i in range(1, len(corr_words)):
137
- if corr_words[i] == corr_words[i-1]:
138
- consecutive_repeats += 1
139
- if consecutive_repeats >= 2:
140
- logger.warning(f"Grammar output has excessive repetition ({consecutive_repeats} repeats), keeping original")
141
- return text
142
-
143
- # 6. Character overlap check: grammar should preserve most of the content
144
- orig_chars = set(text.replace(' ', ''))
145
- corr_chars = set(corrected.replace(' ', ''))
146
- if orig_chars:
147
- overlap = len(orig_chars & corr_chars) / len(orig_chars)
148
- if overlap < 0.5:
149
- logger.warning(f"Grammar output low char overlap ({overlap:.2f}), keeping original")
150
- return text
151
-
152
- return corrected
153
 
154
  except Exception as e:
155
  logger.error(f"Grammar correction failed: {e}")
 
102
  # Decode output
103
  corrected = tokenizer.decode(outputs[0], skip_special_tokens=True)
104
 
105
+ # Safety: if model returns empty or much shorter, keep original
 
 
106
  if not corrected or not corrected.strip():
107
  return text
108
 
109
+ # If the correction is drastically different (>80% changed), keep original
 
 
110
  if len(corrected) < len(text) * 0.3:
111
  logger.warning(f"Grammar output too short ({len(corrected)} vs {len(text)}), keeping original")
112
  return text
113
 
114
+ return corrected.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  except Exception as e:
117
  logger.error(f"Grammar correction failed: {e}")