youssefreda9 commited on
Commit
02d51b7
·
1 Parent(s): 9eee80b

fix: Prevent overlapping spelling+grammar suggestions from duplicating text - Backend: Skip grammar suggestions that overlap with existing spelling ranges - Frontend: applyAllSuggestions skips overlapping ranges to prevent duplication

Browse files
src/app.py CHANGED
@@ -857,57 +857,29 @@ def analyze_text():
857
  from nlp.grammar.grammar_service import correct_grammar
858
  corrected_grammar = correct_grammar(current_text)
859
  logger.info(f"[ANALYZE] Step 2: Grammar done in {time.time()-t0:.2f}s")
860
- logger.info(f"[GRAMMAR] Input: '{current_text}'")
861
- logger.info(f"[GRAMMAR] Output: '{corrected_grammar}'")
862
  if corrected_grammar != current_text:
863
- # Word-level grammar diffs (individual words, not chunks)
864
- g_orig_words = get_word_positions(current_text)
865
- g_corr_words = get_word_positions(corrected_grammar)
866
- g_matcher = difflib.SequenceMatcher(
867
- None,
868
- [w[0] for w in g_orig_words],
869
- [w[0] for w in g_corr_words]
870
- )
871
-
872
- for tag, i1, i2, j1, j2 in g_matcher.get_opcodes():
873
- logger.info(f"[GRAMMAR] opcode: {tag} orig[{i1}:{i2}]={[w[0] for w in g_orig_words[i1:i2]]} → corr[{j1}:{j2}]={[w[0] for w in g_corr_words[j1:j2]]}")
874
- if tag == 'replace':
875
- # Process each original word individually
876
- for k in range(i1, i2):
877
- ck = j1 + (k - i1)
878
- if ck < j2:
879
- o_word = g_orig_words[k][0]
880
- c_word = g_corr_words[ck][0]
881
- if o_word != c_word:
882
- g_start = g_orig_words[k][1]
883
- g_end = g_orig_words[k][2]
884
- orig_start, orig_end = map_range_to_original(g_start, g_end)
885
-
886
- # Skip if overlaps with existing spelling suggestion
887
- overlaps = False
888
- for existing in suggestions:
889
- if existing['type'] == 'spelling':
890
- if not (orig_end <= existing['start'] or orig_start >= existing['end']):
891
- overlaps = True
892
- logger.info(f"[GRAMMAR] SKIP '{o_word}'→'{c_word}' (overlaps with spelling '{existing['original']}'→'{existing['correction']}')")
893
- break
894
- if overlaps:
895
- continue
896
-
897
- logger.info(f"[GRAMMAR] ADD suggestion: '{o_word}' → '{c_word}' at orig[{orig_start}:{orig_end}]")
898
- suggestions.append({
899
- 'start': orig_start,
900
- 'end': orig_end,
901
- 'original': text[orig_start:orig_end],
902
- 'correction': c_word,
903
- 'type': 'grammar',
904
- 'alternatives': [c_word, text[orig_start:orig_end]],
905
- })
906
-
907
  mappers.append(OffsetMapper(current_text, corrected_grammar))
908
  current_text = corrected_grammar
909
- else:
910
- logger.info(f"[GRAMMAR] No changes — grammar output == input")
911
  except Exception as e:
912
  logger.error(f"[ANALYZE] Grammar failed: {e}")
913
 
 
857
  from nlp.grammar.grammar_service import correct_grammar
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
+ # Collect existing suggestion ranges to avoid overlaps
862
+ existing_ranges = [(s['start'], s['end']) for s in suggestions]
863
+ diffs = get_word_diffs(current_text, corrected_grammar)
864
+ for d in diffs:
865
+ orig_start, orig_end = map_range_to_original(d['start'], d['end'])
866
+ # Skip if this range overlaps with any existing suggestion
867
+ overlaps = any(
868
+ not (orig_end <= es or orig_start >= ee)
869
+ for es, ee in existing_ranges
870
+ )
871
+ if overlaps:
872
+ logger.debug(f"[ANALYZE] Skipping grammar suggestion at {orig_start}-{orig_end} (overlaps spelling)")
873
+ continue
874
+ suggestions.append({
875
+ 'start': orig_start,
876
+ 'end': orig_end,
877
+ 'original': text[orig_start:orig_end],
878
+ 'correction': d['correction'],
879
+ 'type': 'grammar'
880
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
881
  mappers.append(OffsetMapper(current_text, corrected_grammar))
882
  current_text = corrected_grammar
 
 
883
  except Exception as e:
884
  logger.error(f"[ANALYZE] Grammar failed: {e}")
885
 
src/js/editor.js CHANGED
@@ -362,8 +362,14 @@ function applyAllSuggestions() {
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));
 
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));
src/nlp/grammar/grammar_service.py CHANGED
@@ -14,7 +14,7 @@ _device = None
14
 
15
  MODEL_ID = "bayan10/Bayan_Arabic_Grammar"
16
 
17
- # Generation parameters — ORIGINAL WORKING VALUES
18
  MAX_INPUT_LENGTH = 512
19
  MAX_OUTPUT_LENGTH = 512
20
  NUM_BEAMS = 4
@@ -89,7 +89,7 @@ def correct_grammar(text: str) -> str:
89
  )
90
  inputs = {k: v.to(device) for k, v in inputs.items()}
91
 
92
- # Generate correction — ORIGINAL WORKING PARAMS
93
  with torch.no_grad():
94
  outputs = model.generate(
95
  **inputs,
@@ -102,14 +102,15 @@ def correct_grammar(text: str) -> str:
102
  # Decode output
103
  corrected = tokenizer.decode(outputs[0], skip_special_tokens=True)
104
 
105
- # Log for debugging
106
- logger.info(f"[GRAMMAR-RAW] input({len(text)}): '{text}'")
107
- logger.info(f"[GRAMMAR-RAW] output({len(corrected)}): '{corrected}'")
108
-
109
- # Safety: if model returns empty, keep original
110
  if not corrected or not corrected.strip():
111
  return text
112
 
 
 
 
 
 
113
  return corrected.strip()
114
 
115
  except Exception as e:
 
14
 
15
  MODEL_ID = "bayan10/Bayan_Arabic_Grammar"
16
 
17
+ # Generation parameters
18
  MAX_INPUT_LENGTH = 512
19
  MAX_OUTPUT_LENGTH = 512
20
  NUM_BEAMS = 4
 
89
  )
90
  inputs = {k: v.to(device) for k, v in inputs.items()}
91
 
92
+ # Generate correction
93
  with torch.no_grad():
94
  outputs = model.generate(
95
  **inputs,
 
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: