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 +20 -48
- src/js/editor.js +6 -0
- src/nlp/grammar/grammar_service.py +8 -7
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 |
-
#
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 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
|
| 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
|
| 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 |
-
#
|
| 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:
|