idnameraj Cursor commited on
Commit
c1447cb
·
1 Parent(s): 807960c

Fix phrase span leaks, safer length budget, disable phrase T5 by default.

Browse files

Reject object-front and duplicate-verb span candidates, keep length budget from dropping source content, and bake ENGINE_PHRASE_USE_T5=false into Space defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>

.env.example CHANGED
@@ -27,13 +27,29 @@ ENGINE_SAFETY_MIN=0.80
27
  ENGINE_USE_MINILM_SAFETY=true
28
  MINILM_MODEL=sentence-transformers/all-MiniLM-L6-v2
29
 
30
- # Local CPU paraphraser for uncovered sentences (preferred over fixed clefts)
31
  ENGINE_PARAPHRASE=true
 
32
  ENGINE_PARAPHRASE_MODEL=Vamsi/T5_Paraphrase_Paws
33
  ENGINE_PARAPHRASE_MIN_SIM=0.72
34
- ENGINE_PARAPHRASE_NUM_RETURN=3
35
- ENGINE_PARAPHRASE_MAX_NEW_TOKENS=64
36
- ENGINE_PARAPHRASE_MAX_SURFACE=0.92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  # Legacy fixed cleft fallback (off by default)
39
  ENGINE_FORCE_REWRITE=false
@@ -43,7 +59,9 @@ ENGINE_REQUIRE_WORDING_CHANGE=true
43
 
44
  # Low-impact vocabulary refinement after structure/paraphrase
45
  ENGINE_LEXICAL_REFINEMENT=true
46
- ENGINE_LEXICAL_MAX_CHANGES=3
 
 
47
  ENGINE_LEXICAL_MIN_WSD=0.14
48
  ENGINE_LEXICAL_MIN_ZIPF=4.0
49
  ENGINE_LEXICAL_MAX_FREQUENCY_GAP=0.60
 
27
  ENGINE_USE_MINILM_SAFETY=true
28
  MINILM_MODEL=sentence-transformers/all-MiniLM-L6-v2
29
 
30
+ # Local CPU paraphraser for meaning-safe but newer-looking rewrites
31
  ENGINE_PARAPHRASE=true
32
+ ENGINE_PARAPHRASE_PRIMARY=true
33
  ENGINE_PARAPHRASE_MODEL=Vamsi/T5_Paraphrase_Paws
34
  ENGINE_PARAPHRASE_MIN_SIM=0.72
35
+ ENGINE_PARAPHRASE_NUM_RETURN=5
36
+ ENGINE_PARAPHRASE_MAX_NEW_TOKENS=72
37
+ ENGINE_PARAPHRASE_MAX_SURFACE=0.88
38
+ ENGINE_PARAPHRASE_MIN_DIVERGENCE=0.14
39
+
40
+ # Phrase-level rewrite (verb–object spans via WordNet hyponyms + optional T5)
41
+ ENGINE_PHRASE_REWRITE=true
42
+ # 0 = dynamic (1 normally, 2 with polish). Set 1–4 to hard-cap.
43
+ ENGINE_PHRASE_MAX_CHANGES=0
44
+ ENGINE_PHRASE_MIN_SIM=0.74
45
+ # Keep T5 span paraphrase off by default (WordNet phrase path is safer).
46
+ ENGINE_PHRASE_USE_T5=false
47
+
48
+ # Keep rewrite length near the source; trim only when clearly bloated.
49
+ ENGINE_PRESERVE_LENGTH=true
50
+ # Split long sentences on clause joins during structural fallback.
51
+ ENGINE_SPLIT_LONG=true
52
+ ENGINE_SPLIT_MIN_WORDS=16
53
 
54
  # Legacy fixed cleft fallback (off by default)
55
  ENGINE_FORCE_REWRITE=false
 
59
 
60
  # Low-impact vocabulary refinement after structure/paraphrase
61
  ENGINE_LEXICAL_REFINEMENT=true
62
+ # 0 = fully dynamic synonym budget from sentence length (recommended).
63
+ # Set >0 only to hard-cap the dynamic budget (max 15).
64
+ ENGINE_LEXICAL_MAX_CHANGES=0
65
  ENGINE_LEXICAL_MIN_WSD=0.14
66
  ENGINE_LEXICAL_MIN_ZIPF=4.0
67
  ENGINE_LEXICAL_MAX_FREQUENCY_GAP=0.60
Dockerfile CHANGED
@@ -20,7 +20,11 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
20
  ENGINE_PARAPHRASE=true \
21
  ENGINE_FORCE_REWRITE=false \
22
  ENGINE_USE_MINILM_SAFETY=true \
23
- ENGINE_LEXICAL_REFINEMENT=true \
 
 
 
 
24
  HF_HOME=/opt/hf_cache \
25
  TRANSFORMERS_CACHE=/opt/hf_cache \
26
  LANGUAGETOOL_HOME=/opt/languagetool \
 
20
  ENGINE_PARAPHRASE=true \
21
  ENGINE_FORCE_REWRITE=false \
22
  ENGINE_USE_MINILM_SAFETY=true \
23
+ ENGINE_LEXICAL_REFINEMENT=true \
24
+ ENGINE_PHRASE_REWRITE=true \
25
+ ENGINE_PHRASE_USE_T5=false \
26
+ ENGINE_PRESERVE_LENGTH=true \
27
+ ENGINE_SPLIT_LONG=true \\
28
  HF_HOME=/opt/hf_cache \
29
  TRANSFORMERS_CACHE=/opt/hf_cache \
30
  LANGUAGETOOL_HOME=/opt/languagetool \
app/config.py CHANGED
@@ -209,6 +209,9 @@ ENGINE_PHRASE_MIN_SIM = max(
209
  0.0,
210
  min(float(os.environ.get("ENGINE_PHRASE_MIN_SIM", "0.74") or "0.74"), 1.0),
211
  )
 
 
 
212
 
213
  # Keep rewritten length near the source (trims only when clearly bloated).
214
  _epl = (os.environ.get("ENGINE_PRESERVE_LENGTH") or "true").strip().lower()
 
209
  0.0,
210
  min(float(os.environ.get("ENGINE_PHRASE_MIN_SIM", "0.74") or "0.74"), 1.0),
211
  )
212
+ # T5 span paraphrase is optional; WordNet phrase swaps are the default wording path.
213
+ _ephr_t5 = (os.environ.get("ENGINE_PHRASE_USE_T5") or "false").strip().lower()
214
+ ENGINE_PHRASE_USE_T5 = _ephr_t5 in {"1", "true", "yes", "on"}
215
 
216
  # Keep rewritten length near the source (trims only when clearly bloated).
217
  _epl = (os.environ.get("ENGINE_PRESERVE_LENGTH") or "true").strip().lower()
app/engine/mechanics.py CHANGED
@@ -22,7 +22,8 @@ def enforce_length_budget(
22
  ) -> str:
23
  """Trim only when the rewrite clearly ballooned past the original.
24
 
25
- Preserves paragraph breaks (\\n\\n) never flattens the document.
 
26
  """
27
  source = original or ""
28
  output = rewritten or ""
@@ -31,8 +32,11 @@ def enforce_length_budget(
31
  if source_words == 0:
32
  return output
33
 
34
- max_ratio = 1.08 if preserve_length else 1.6
 
35
  max_words = max(1, int(source_words * max_ratio))
 
 
36
  if output_words <= max_words:
37
  return output
38
 
@@ -59,4 +63,8 @@ def enforce_length_budget(
59
  kept_paragraphs.append(" ".join(kept_sentences))
60
  if count >= max_words:
61
  break
62
- return "\n\n".join(kept_paragraphs).strip() or output
 
 
 
 
 
22
  ) -> str:
23
  """Trim only when the rewrite clearly ballooned past the original.
24
 
25
+ Preserves paragraph breaks (\\n\\n). Never trims so hard that most of the
26
+ source content disappears.
27
  """
28
  source = original or ""
29
  output = rewritten or ""
 
32
  if source_words == 0:
33
  return output
34
 
35
+ # Slightly looser ceiling so normal phrase/lexical growth is kept.
36
+ max_ratio = 1.20 if preserve_length else 1.6
37
  max_words = max(1, int(source_words * max_ratio))
38
+ # Floor: do not destroy content just to hit the ceiling.
39
+ min_words = max(1, int(source_words * 0.90))
40
  if output_words <= max_words:
41
  return output
42
 
 
63
  kept_paragraphs.append(" ".join(kept_sentences))
64
  if count >= max_words:
65
  break
66
+ trimmed = "\n\n".join(kept_paragraphs).strip()
67
+ if not trimmed or len(trimmed.split()) < min_words:
68
+ # Trimming would drop too much meaning — keep the full rewrite.
69
+ return output
70
+ return trimmed
app/engine/orchestrator.py CHANGED
@@ -18,6 +18,7 @@ from app.config import (
18
  ENGINE_PHRASE_MAX_CHANGES,
19
  ENGINE_PHRASE_MIN_SIM,
20
  ENGINE_PHRASE_REWRITE,
 
21
  ENGINE_PRESERVE_LENGTH,
22
  ENGINE_REQUIRE_WORDING_CHANGE,
23
  ENGINE_SAFETY_MIN,
@@ -211,6 +212,7 @@ def _apply_phrase_rewrite(
211
  max_changes=max_changes,
212
  polish=polish,
213
  min_sim=min(ENGINE_PHRASE_MIN_SIM, safety_min),
 
214
  )
215
  if not refined.changes or refined.text == before:
216
  return record
 
18
  ENGINE_PHRASE_MAX_CHANGES,
19
  ENGINE_PHRASE_MIN_SIM,
20
  ENGINE_PHRASE_REWRITE,
21
+ ENGINE_PHRASE_USE_T5,
22
  ENGINE_PRESERVE_LENGTH,
23
  ENGINE_REQUIRE_WORDING_CHANGE,
24
  ENGINE_SAFETY_MIN,
 
212
  max_changes=max_changes,
213
  polish=polish,
214
  min_sim=min(ENGINE_PHRASE_MIN_SIM, safety_min),
215
+ use_t5=ENGINE_PHRASE_USE_T5,
216
  )
217
  if not refined.changes or refined.text == before:
218
  return record
app/engine/paraphrase/__init__.py CHANGED
@@ -414,19 +414,35 @@ def _span_candidate_ok(source: str, candidate: str) -> bool:
414
  return False
415
  if ":" in cand or ";" in cand:
416
  return False
417
- src_tokens = _WORD.findall(src)
418
- cand_tokens = _WORD.findall(cand)
419
  if not src_tokens or not cand_tokens:
420
  return False
421
  # Keep span length close so splicing stays grammatical.
422
- if len(cand_tokens) > len(src_tokens) + 2 or len(cand_tokens) + 2 < len(src_tokens):
423
  return False
424
- if len(cand) > max(12, int(len(src) * 1.6)):
425
  return False
426
  # Mid-sentence VO spans usually start with a verb; reject full-clause flips
427
- # like "Customer loyalty strengthens".
428
  if src[0].islower() and cand[0].isupper():
429
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  return True
431
 
432
 
 
414
  return False
415
  if ":" in cand or ";" in cand:
416
  return False
417
+ src_tokens = [token.lower() for token in _WORD.findall(src)]
418
+ cand_tokens = [token.lower() for token in _WORD.findall(cand)]
419
  if not src_tokens or not cand_tokens:
420
  return False
421
  # Keep span length close so splicing stays grammatical.
422
+ if len(cand_tokens) > len(src_tokens) + 1 or len(cand_tokens) + 2 < len(src_tokens):
423
  return False
424
+ if len(cand) > max(12, int(len(src) * 1.45)):
425
  return False
426
  # Mid-sentence VO spans usually start with a verb; reject full-clause flips
427
+ # like "Customer loyalty strengthens" / "customer loyalty strengthens".
428
  if src[0].islower() and cand[0].isupper():
429
  return False
430
+ src_tail = set(src_tokens[1:])
431
+ if cand_tokens[0] in src_tail:
432
+ return False
433
+ # Reject duplicated content words ("…experience to create").
434
+ from collections import Counter
435
+
436
+ src_counts = Counter(src_tokens)
437
+ cand_counts = Counter(cand_tokens)
438
+ for token, count in cand_counts.items():
439
+ if len(token) < 4:
440
+ continue
441
+ if count > max(1, src_counts.get(token, 0)):
442
+ return False
443
+ # Reject dangling infinitive tails invented by T5.
444
+ if re.search(r"\bto\s+[a-z]{3,}$", low) and " to " not in src.lower():
445
+ return False
446
  return True
447
 
448