idnameraj commited on
Commit
4b7dfdd
·
verified ·
1 Parent(s): afd164d

Upload 107 files

Browse files
app/pipeline/__pycache__/orchestrator.cpython-311.pyc CHANGED
Binary files a/app/pipeline/__pycache__/orchestrator.cpython-311.pyc and b/app/pipeline/__pycache__/orchestrator.cpython-311.pyc differ
 
app/pipeline/candidate_validator.py CHANGED
@@ -200,6 +200,7 @@ _WEAK_TOPIC_HEADS = frozenset(
200
  local young loud hard free quiet small large great important different
201
  second first major main recent modern social early late daily
202
  without within enough several various certain further additional
 
203
  """.split()
204
  )
205
 
 
200
  local young loud hard free quiet small large great important different
201
  second first major main recent modern social early late daily
202
  without within enough several various certain further additional
203
+ unfortunately sadly however therefore moreover furthermore actually
204
  """.split()
205
  )
206
 
app/pipeline/orchestrator.py CHANGED
@@ -121,78 +121,172 @@ def _ensure_unit_terminal(text: str) -> str:
121
  return s
122
 
123
 
124
- def _safe_paraphrase_trial(source_unit: str, trial: str) -> tuple[bool, float]:
125
- """Return (ok_for_visible_use, surface_sim). Soft-only validation failures allowed."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  t = (trial or "").strip()
127
  if not t:
128
- return False, 1.0
129
  t = _clamp_to_source_shape(source_unit, t) or t
130
- t = _ensure_unit_terminal(t)
131
- v = validate_candidate(source_unit, t, min_meaning=0.70, max_surface=0.995)
132
- soft = {"too_similar", "identical", "near_copy", "too_divergent"}
133
- hard_keys = [
134
- r.split(":")[0] for r in v.reasons if r.split(":")[0] not in soft
135
- ]
 
 
 
 
 
 
 
 
 
136
  if _hard_fail(hard_keys):
137
- return False, 1.0
138
- return True, _similarity_ratio(source_unit, t)
 
 
139
 
140
 
141
- _SAFE_SURFACE_SWAPS: tuple[tuple[re.Pattern[str], str], ...] = (
142
- (re.compile(r"\bIn addition,\b", re.I), "Also,"),
143
- (re.compile(r"\bAdditionally,\b", re.I), "Also,"),
144
- (re.compile(r"\bHowever,\b", re.I), "Still,"),
145
- (re.compile(r"\bTherefore,\b", re.I), "So,"),
146
- (re.compile(r"\bDue to the fact that\b", re.I), "Because"),
147
- (re.compile(r"\bin order to\b", re.I), "to"),
148
- (re.compile(r"\ba large number of\b", re.I), "many"),
149
- (re.compile(r"\bMany\b"), "A lot of"),
150
- (re.compile(r"\bA lot of\b", re.I), "Many"),
151
- (re.compile(r"\bis able to\b", re.I), "can"),
152
- (re.compile(r"\bare able to\b", re.I), "can"),
153
- (re.compile(r"\bPeople should\b"), "People need to"),
154
- (re.compile(r"\bpeople should\b"), "people need to"),
155
- (re.compile(r"\binstead of\b", re.I), "rather than"),
156
- (re.compile(r"\brather than\b", re.I), "instead of"),
157
- (re.compile(r"\bbecause of\b", re.I), "due to"),
158
- (re.compile(r"\bfor example\b", re.I), "for instance"),
159
- (re.compile(r"\bfor instance\b", re.I), "for example"),
160
- (re.compile(r"\bthink that\b", re.I), "think"),
161
- (re.compile(r"\bbelieve that\b", re.I), "believe"),
162
- (re.compile(r"\bsay that\b", re.I), "say"),
163
- (re.compile(r"\bis too\b", re.I), "is really too"),
164
- (re.compile(r"\bare too\b", re.I), "are really too"),
165
- (re.compile(r"\bbetter for (.+?) than\b", re.I), r"better for \1 compared to"),
166
- (re.compile(r"\bbetter for (.+?) compared to\b", re.I), r"better for \1 than"),
167
- (re.compile(r"\bby giving\b", re.I), "by offering"),
168
- (re.compile(r"\bhelp communities\b", re.I), "support communities"),
169
- (re.compile(r"\bwastes their\b", re.I), "uses up their"),
170
- (re.compile(r"\bfind it harder to\b", re.I), "have a harder time"),
171
- (re.compile(r"\bhave a harder time\b", re.I), "find it harder to"),
172
- (re.compile(r"\bit is important to\b", re.I), "people need to"),
173
- (re.compile(r"\bThere are many\b"), "Plenty of"),
174
- )
175
 
176
- _VISIBLE_TARGET_SIM = 0.85
177
- _LM_SHIP_MAX_SIM = 0.88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
 
180
- def _cap_first(text: str) -> str:
181
- t = (text or "").strip()
 
182
  if not t:
183
- return t
184
- return t[0].upper() + t[1:]
 
 
 
185
 
186
 
187
- def _lower_first(text: str) -> str:
188
- t = (text or "").strip()
189
- if not t:
190
- return t
191
- return t[0].lower() + t[1:]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
 
194
  def _esl_structural_variants(text: str) -> list[str]:
195
- """High-value ESL / claim rewrites that change wording while keeping meaning."""
196
  out: list[str] = []
197
  base = (text or "").strip()
198
  if not base:
@@ -201,7 +295,6 @@ def _esl_structural_variants(text: str) -> list[str]:
201
  end = end_m.group(0) if end_m else "."
202
  core = base[: end_m.start()] if end_m else base
203
 
204
- # Another/One important thing is/are X → keep important/thing stems + topic
205
  m = re.match(
206
  r"^(?:Another|One|An)\s+important\s+thing\s+(?:is|are)\s+(.+)$",
207
  core,
@@ -210,26 +303,18 @@ def _esl_structural_variants(text: str) -> list[str]:
210
  if m:
211
  topic = m.group(1).strip().rstrip(".!?")
212
  if topic:
213
- out.append(f"{_cap_first(topic)} is another important thing{end}")
214
- out.append(f"{_cap_first(topic)} is also an important thing{end}")
215
- out.append(f"One more important thing is {_lower_first(topic)}{end}")
216
 
217
- # Without enough X, Y → reorder / rephrase while keeping enough+X(+Y)
218
  m = re.match(r"^Without enough\s+(\w+),\s*(.+)$", core, flags=re.I)
219
  if m:
220
  noun, rest = m.group(1), m.group(2).strip()
221
  if noun and rest:
222
- # Clause reorder keeps all stems (best for incomplete fragments)
223
- out.append(f"{_cap_first(rest)} without enough {noun}{end}")
224
- out.append(
225
- f"When there is not enough {noun}, {_lower_first(rest)}{end}"
226
- )
227
- out.append(f"With not enough {noun}, {_lower_first(rest)}{end}")
228
- out.append(
229
- f"If there is not enough {noun}, {_lower_first(rest)}{end}"
230
- )
231
 
232
- # X is one of the most important Y
233
  m = re.match(
234
  r"^(.+?)\s+is one of the most important\s+(.+)$",
235
  core,
@@ -238,50 +323,80 @@ def _esl_structural_variants(text: str) -> list[str]:
238
  if m:
239
  subj, rest = m.group(1).strip(), m.group(2).strip()
240
  if subj and rest:
241
- out.append(f"{_cap_first(subj)} is a very important part of {rest}{end}")
242
- out.append(
243
- f"Among the most important {rest}, {_lower_first(subj)} stands out{end}"
244
- )
245
 
246
- # It is important that/to …
247
  m = re.match(r"^It is important (?:that|to)\s+(.+)$", core, flags=re.I)
248
  if m:
249
  rest = m.group(1).strip()
250
  if rest:
251
  out.append(f"People need to {_lower_first(rest)}{end}")
252
- out.append(f"{_cap_first(rest)} is important{end}")
 
 
 
 
 
 
 
 
 
 
 
253
 
254
  return out
255
 
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  def _surface_humanize_variants(text: str, rng: random.Random) -> list[str]:
258
- """Stem-preserving phrase/style variants prefer these over aggressive lexicon thrash."""
259
  out: list[str] = []
260
  base = (text or "").strip()
261
  if not base:
262
  return out
263
 
 
264
  out.extend(_esl_structural_variants(base))
265
 
266
- # Intensifier insert before adjectives after "too/very/so"
267
- m = re.search(r"\b(too|very|so)\s+([a-zA-Z]{4,})\b", base)
268
- if m and "really" not in base.lower():
269
- out.append(base[: m.start()] + f"really {m.group(0)}" + base[m.end() :])
270
-
271
- # Front "Instead of X, Y" from "Y instead of X"
272
- m = re.search(
273
- r"^(?P<main>.+?)\s+instead of\s+(?P<alt>.+?)(?P<end>[.!?]?)$",
274
- base,
275
- flags=re.I,
276
- )
277
- if m:
278
- main = m.group("main").rstrip(" ,")
279
- alt = m.group("alt").rstrip(" .!?")
280
- end = m.group("end") or "."
281
- main_l = _lower_first(main)
282
- out.append(f"Instead of {alt}, {main_l}{end}")
283
-
284
- # Because-clause fronting: "X because Y" → "Because Y, X"
285
  m = re.match(
286
  r"^(?P<main>.+?)\s+because\s+(?P<reason>.+?)(?P<end>[.!?])?$",
287
  base,
@@ -301,18 +416,10 @@ def _surface_humanize_variants(text: str, rng: random.Random) -> list[str]:
301
  cand = pat.sub(repl, base, count=1)
302
  if cand.strip() and cand.strip() != base:
303
  out.append(cand.strip())
304
- stacked = base
305
- applied = 0
306
- for pat, repl in swaps:
307
- if applied >= 2:
308
- break
309
- if pat.search(stacked):
310
- nxt = pat.sub(repl, stacked, count=1)
311
- if nxt != stacked:
312
- stacked = nxt
313
- applied += 1
314
- if stacked.strip() and stacked.strip() != base:
315
- out.append(stacked.strip())
316
  return out
317
 
318
 
@@ -324,7 +431,7 @@ def _visible_unit_paraphrase(
324
  strength: int = 2,
325
  target_sim: float = _VISIBLE_TARGET_SIM,
326
  ) -> str:
327
- """Force visible wording change on revert surface + structure + light lexicon."""
328
  base = scrub_phrases(source_unit)
329
  base = apply_tone_contractions(base, tone)
330
  base = tidy(correct_text(base) if base else "")
@@ -344,26 +451,33 @@ def _visible_unit_paraphrase(
344
  structured = base
345
 
346
  trials: list[str] = []
347
- trials.extend(_esl_structural_variants(base))
348
- trials.extend(_surface_humanize_variants(base, rng))
349
- trials.extend(_surface_humanize_variants(structured, rng))
 
350
  if structured.strip() and structured.strip() != base.strip():
351
  trials.append(structured)
352
 
353
- for syn_src, syn_str in ((structured, 1), (base, 1), (structured, 2), (base, 2)):
 
 
 
 
 
 
354
  try:
355
  cand = rewrite_sentence_synonyms(
356
  syn_src,
357
  syn_str,
358
  rng,
359
  tone=tone,
360
- force_all_lexicon=False,
361
  )
362
  cand = apply_tone_contractions(cand, tone)
363
  cand = tidy(correct_text(cand) if cand else "")
364
  if cand.strip():
365
  trials.append(cand)
366
- trials.extend(_surface_humanize_variants(cand, rng))
367
  trials.extend(_esl_structural_variants(cand))
368
  except Exception:
369
  continue
@@ -378,7 +492,7 @@ def _visible_unit_paraphrase(
378
  uniq.append(t.strip())
379
 
380
  best = _ensure_unit_terminal(source_unit)
381
- best_sim = 1.0
382
  for trial in uniq:
383
  ok, sim = _safe_paraphrase_trial(source_unit, trial)
384
  if not ok:
@@ -386,13 +500,26 @@ def _visible_unit_paraphrase(
386
  t = _ensure_unit_terminal(
387
  _clamp_to_source_shape(source_unit, trial.strip()) or trial.strip()
388
  )
389
- if t.lower().rstrip(".!?") == source_unit.lower().rstrip(".!?"):
390
- continue
391
- if sim < best_sim:
 
392
  best = t
393
- best_sim = sim
394
- if sim <= target_sim:
395
- break
 
 
 
 
 
 
 
 
 
 
 
 
396
  return best
397
 
398
 
@@ -403,7 +530,7 @@ def _light_revert_polish(
403
  *,
404
  strength: int = 0,
405
  ) -> str:
406
- """Backward-compatible name — now runs visible classical paraphrase."""
407
  return _visible_unit_paraphrase(
408
  source_unit,
409
  tone,
@@ -422,49 +549,34 @@ def _require_visible_rewrite(
422
  target_sim: float = _VISIBLE_TARGET_SIM,
423
  min_words: int = 4,
424
  ) -> str:
425
- """Never ship grammar-only near-copies when a safe different sentence exists."""
426
  src = (source_unit or "").strip()
427
  cand = (candidate or src).strip() or src
428
  if len(src.split()) < min_words:
429
  return _ensure_unit_terminal(cand)
430
 
431
- cand_sim = _similarity_ratio(src, cand)
432
- if (
433
- cand_sim <= target_sim
434
- and cand.lower().rstrip(".!?") != src.lower().rstrip(".!?")
435
- ):
436
- ok, _ = _safe_paraphrase_trial(src, cand)
437
- if ok:
438
- return _ensure_unit_terminal(cand)
439
 
440
- # Prefer classical paraphrase of source (not of a hollow LM near-copy)
441
  trials = [
442
  _visible_unit_paraphrase(src, tone, rng, strength=2, target_sim=target_sim),
443
- _visible_unit_paraphrase(src, tone, rng, strength=2, target_sim=0.80),
444
  ]
445
- if cand.lower().rstrip(".!?") != src.lower().rstrip(".!?"):
446
- trials.append(
447
- _visible_unit_paraphrase(
448
- cand, tone, rng, strength=2, target_sim=target_sim
449
- )
450
- )
451
-
452
  best = _ensure_unit_terminal(cand)
453
- best_sim = cand_sim
 
 
454
  for trial in trials:
455
- ok, sim = _safe_paraphrase_trial(src, trial)
456
- if not ok:
457
  continue
458
  t = _ensure_unit_terminal(
459
  _clamp_to_source_shape(src, trial.strip()) or trial.strip()
460
  )
461
- if t.lower().rstrip(".!?") == src.lower().rstrip(".!?"):
462
- continue
463
- if sim < best_sim:
464
  best = t
465
- best_sim = sim
466
- if sim <= target_sim:
467
- break
468
  return best
469
 
470
 
@@ -476,7 +588,7 @@ def _boost_if_near_copy(
476
  *,
477
  threshold: float = _VISIBLE_TARGET_SIM,
478
  ) -> str:
479
- """If a kept unit is still nearly identical, force classical paraphrase."""
480
  return _require_visible_rewrite(
481
  source_unit,
482
  candidate,
@@ -701,30 +813,38 @@ def _looks_needs_lm(text: str, strength: int) -> bool:
701
  def _accept_lm_unit(
702
  source_unit: str, candidate: str, *, max_sim: float = _LM_SHIP_MAX_SIM
703
  ) -> tuple[bool, list[str]]:
704
- """Strict gate for a single LM-rewritten sentence."""
705
  if not candidate or not candidate.strip():
706
  return False, ["empty"]
707
- # Hard 1:1 sentence shape before other gates
708
  o_n = _sentence_count_simple(source_unit)
709
  c_n = _sentence_count_simple(candidate)
710
  if o_n == 1 and c_n != 1:
711
  return False, ["shape"]
712
  if o_n >= 1 and c_n < o_n:
713
  return False, ["shape"]
 
 
 
 
 
 
714
  v = validate_candidate(
715
- source_unit, candidate, min_meaning=0.80, max_surface=max_sim
716
  )
717
- reasons = list(v.reasons)
718
- sim = _similarity_ratio(source_unit, candidate)
719
- if sim >= max_sim:
720
- reasons.append("near_copy")
721
- # Even if validate passed soft gates, do not ship near-copies as LM success
722
- if sim >= _LM_SHIP_MAX_SIM:
723
- if "near_copy" not in reasons:
724
- reasons.append("near_copy")
725
- return False, reasons or ["near_copy"]
726
- if not v.ok or "near_copy" in reasons:
727
- return False, reasons or ["reject"]
 
 
 
728
  return True, []
729
 
730
 
 
121
  return s
122
 
123
 
124
+ def _unit_stems(text: str) -> set[str]:
125
+ """Content stems shared with the validator (for lexical-novelty checks)."""
126
+ from app.pipeline.candidate_validator import _content_tokens
127
+
128
+ return _content_tokens(text or "")
129
+
130
+
131
+ def _stem_keep_ratio(source: str, candidate: str) -> float:
132
+ src = _unit_stems(source)
133
+ if not src:
134
+ return 1.0
135
+ return len(src & _unit_stems(candidate)) / len(src)
136
+
137
+
138
+ def _stem_jaccard(source: str, candidate: str) -> float:
139
+ a, b = _unit_stems(source), _unit_stems(candidate)
140
+ if not a and not b:
141
+ return 1.0
142
+ return len(a & b) / max(1, len(a | b))
143
+
144
+
145
+ def _paraphrase_hard_ok(source_unit: str, trial: str) -> bool:
146
+ """Meaning safety for forced rewrites.
147
+
148
+ Allows real synonym/phrase changes (relaxed key_content/collocation) but still
149
+ blocks polarity flips, invention, topic swaps, etc.
150
+ """
151
  t = (trial or "").strip()
152
  if not t:
153
+ return False
154
  t = _clamp_to_source_shape(source_unit, t) or t
155
+ v = validate_candidate(source_unit, t, min_meaning=0.62, max_surface=0.995)
156
+ # key_content/coverage/collocation/drift are too strict for synonym paraphrases;
157
+ # enforce a softer stem floor instead.
158
+ soft = {
159
+ "too_similar",
160
+ "identical",
161
+ "near_copy",
162
+ "too_divergent",
163
+ "key_content",
164
+ "coverage",
165
+ "drift",
166
+ "grammar_worse",
167
+ "collocation",
168
+ }
169
+ hard_keys = [r.split(":")[0] for r in v.reasons if r.split(":")[0] not in soft]
170
  if _hard_fail(hard_keys):
171
+ return False
172
+ if _stem_keep_ratio(source_unit, t) < 0.30:
173
+ return False
174
+ return True
175
 
176
 
177
+ def _is_true_rewrite(source_unit: str, candidate: str) -> bool:
178
+ """True rewrite = different wording, not grammar-fix or clause shuffle.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
+ Pure reorder / template edits keep almost the same content stems and must fail.
181
+ """
182
+ src = (source_unit or "").strip()
183
+ cand = (candidate or "").strip()
184
+ if not src or not cand:
185
+ return False
186
+ if cand.lower().rstrip(".!?") == src.lower().rstrip(".!?"):
187
+ return False
188
+ if not _paraphrase_hard_ok(src, cand):
189
+ return False
190
+ sim = _similarity_ratio(src, cand)
191
+ jacc = _stem_jaccard(src, cand)
192
+ # Same bag of content words → reorder/template only (user complaint)
193
+ if jacc >= 0.92:
194
+ return False
195
+ if sim > 0.90:
196
+ return False
197
+ return True
198
 
199
 
200
+ def _safe_paraphrase_trial(source_unit: str, trial: str) -> tuple[bool, float]:
201
+ """Return (is_true_rewrite, surface_sim)."""
202
+ t = (trial or "").strip()
203
  if not t:
204
+ return False, 1.0
205
+ t = _ensure_unit_terminal(_clamp_to_source_shape(source_unit, t) or t)
206
+ if not _is_true_rewrite(source_unit, t):
207
+ return False, 1.0
208
+ return True, _similarity_ratio(source_unit, t)
209
 
210
 
211
+ def _rewrite_score(source_unit: str, candidate: str) -> float:
212
+ """Higher = more different while still usable (lower sim + lower stem overlap)."""
213
+ sim = _similarity_ratio(source_unit, candidate)
214
+ jacc = _stem_jaccard(source_unit, candidate)
215
+ return (1.0 - sim) + 0.75 * (1.0 - jacc)
216
+
217
+
218
+ # Phrase-level meaning-preserving rewrites (change wording, not just order).
219
+ # Prefer swaps that keep enough claim stems to stay meaning-safe.
220
+ _PHRASE_REWRITES: tuple[tuple[re.Pattern[str], str], ...] = (
221
+ (re.compile(r"\bget(?:s)?\s+diseases\b", re.I), "get sick"),
222
+ (re.compile(r"\bgetting\s+diseases\b", re.I), "getting sick"),
223
+ (
224
+ re.compile(r"\bless chance to gets?\s+diseases\b", re.I),
225
+ "less chance to get sick",
226
+ ),
227
+ (
228
+ re.compile(r"\bless chance of getting\s+diseases\b", re.I),
229
+ "less chance of getting sick",
230
+ ),
231
+ (re.compile(r"\bwatching television\b", re.I), "watching TV"),
232
+ (re.compile(r"\bdoing physical activities\b", re.I), "working out"),
233
+ (re.compile(r"\bphysical activities\b", re.I), "real exercise"),
234
+ (re.compile(r"\bexercise regularly\b", re.I), "work out often"),
235
+ (re.compile(r"\bexercises regularly\b", re.I), "works out often"),
236
+ (re.compile(r"\ba person exercise\b", re.I), "a person works out"),
237
+ (re.compile(r"\ba person exercises\b", re.I), "a person works out"),
238
+ (re.compile(r"\bprefers watching\b", re.I), "would rather watch"),
239
+ (re.compile(r"\bprefer watching\b", re.I), "would rather watch"),
240
+ (re.compile(r"\bmany peoples\b", re.I), "a lot of people"),
241
+ (re.compile(r"\bmany people\b", re.I), "a lot of people"),
242
+ (re.compile(r"\bUnfortunately,\s*", re.I), "Sadly, "),
243
+ (
244
+ re.compile(
245
+ r"\bis one of the most important\s+\w+\s+that keeps our body fit\b",
246
+ re.I,
247
+ ),
248
+ "is a key way to keep the body fit",
249
+ ),
250
+ (
251
+ re.compile(r"\bis one of the most important\b", re.I),
252
+ "is a key",
253
+ ),
254
+ (re.compile(r"\bkeeps our body fit\b", re.I), "helps the body stay fit"),
255
+ (re.compile(r"\bfind it harder to\b", re.I), "struggle more to"),
256
+ (re.compile(r"\binstead of\b", re.I), "rather than"),
257
+ )
258
+
259
+
260
+ def _apply_phrase_rewrites(text: str, *, max_swaps: int = 4) -> list[str]:
261
+ """Build increasingly rewritten phrase variants."""
262
+ out: list[str] = []
263
+ base = (text or "").strip()
264
+ if not base:
265
+ return out
266
+ # Single-swap variants
267
+ for pat, repl in _PHRASE_REWRITES:
268
+ if not pat.search(base):
269
+ continue
270
+ cand = pat.sub(repl, base, count=1)
271
+ if cand.strip() and cand.strip() != base:
272
+ out.append(cand.strip())
273
+ # Stacked multi-swap
274
+ stacked = base
275
+ n = 0
276
+ for pat, repl in _PHRASE_REWRITES:
277
+ if n >= max_swaps:
278
+ break
279
+ if pat.search(stacked):
280
+ nxt = pat.sub(repl, stacked, count=1)
281
+ if nxt != stacked:
282
+ stacked = nxt
283
+ n += 1
284
+ out.append(stacked.strip())
285
+ return out
286
 
287
 
288
  def _esl_structural_variants(text: str) -> list[str]:
289
+ """Lexical ESL rewrites (must change wording not mere reorder)."""
290
  out: list[str] = []
291
  base = (text or "").strip()
292
  if not base:
 
295
  end = end_m.group(0) if end_m else "."
296
  core = base[: end_m.start()] if end_m else base
297
 
 
298
  m = re.match(
299
  r"^(?:Another|One|An)\s+important\s+thing\s+(?:is|are)\s+(.+)$",
300
  core,
 
303
  if m:
304
  topic = m.group(1).strip().rstrip(".!?")
305
  if topic:
306
+ out.append(f"{_cap_first(topic)} also matters a lot{end}")
307
+ out.append(f"Getting {_lower_first(topic)} is also important{end}")
308
+ out.append(f"{_cap_first(topic)} deserves attention too{end}")
309
 
 
310
  m = re.match(r"^Without enough\s+(\w+),\s*(.+)$", core, flags=re.I)
311
  if m:
312
  noun, rest = m.group(1), m.group(2).strip()
313
  if noun and rest:
314
+ out.append(f"When {noun} runs short, {_lower_first(rest)}{end}")
315
+ out.append(f"If {noun} is lacking, {_lower_first(rest)}{end}")
316
+ out.append(f"Too little {noun} leaves {_lower_first(rest)}{end}")
 
 
 
 
 
 
317
 
 
318
  m = re.match(
319
  r"^(.+?)\s+is one of the most important\s+(.+)$",
320
  core,
 
323
  if m:
324
  subj, rest = m.group(1).strip(), m.group(2).strip()
325
  if subj and rest:
326
+ out.append(f"{_cap_first(subj)} is a key part of {rest}{end}")
327
+ out.append(f"{_cap_first(subj)} matters a lot for {rest}{end}")
 
 
328
 
 
329
  m = re.match(r"^It is important (?:that|to)\s+(.+)$", core, flags=re.I)
330
  if m:
331
  rest = m.group(1).strip()
332
  if rest:
333
  out.append(f"People need to {_lower_first(rest)}{end}")
334
+ out.append(f"{_cap_first(rest)} really matters{end}")
335
+
336
+ # If a person X, they will Y → People who X will Y
337
+ m = re.match(
338
+ r"^(?:If\s+)?a person\s+(.+?),\s*they will\s+(.+)$",
339
+ core,
340
+ flags=re.I,
341
+ )
342
+ if m:
343
+ act, result = m.group(1).strip(), m.group(2).strip()
344
+ out.append(f"People who {act} will {result}{end}")
345
+ out.append(f"Anyone who {act} will {result}{end}")
346
 
347
  return out
348
 
349
 
350
+ def _cap_first(text: str) -> str:
351
+ t = (text or "").strip()
352
+ if not t:
353
+ return t
354
+ return t[0].upper() + t[1:]
355
+
356
+
357
+ def _lower_first(text: str) -> str:
358
+ t = (text or "").strip()
359
+ if not t:
360
+ return t
361
+ return t[0].lower() + t[1:]
362
+
363
+
364
+ _SAFE_SURFACE_SWAPS: tuple[tuple[re.Pattern[str], str], ...] = (
365
+ (re.compile(r"\bIn addition,\b", re.I), "Also,"),
366
+ (re.compile(r"\bAdditionally,\b", re.I), "Also,"),
367
+ (re.compile(r"\bHowever,\b", re.I), "Still,"),
368
+ (re.compile(r"\bTherefore,\b", re.I), "So,"),
369
+ (re.compile(r"\bDue to the fact that\b", re.I), "Because"),
370
+ (re.compile(r"\bin order to\b", re.I), "to"),
371
+ (re.compile(r"\ba large number of\b", re.I), "many"),
372
+ (re.compile(r"\bMany\b"), "A lot of"),
373
+ (re.compile(r"\bis able to\b", re.I), "can"),
374
+ (re.compile(r"\bare able to\b", re.I), "can"),
375
+ (re.compile(r"\bPeople should\b"), "People need to"),
376
+ (re.compile(r"\bpeople should\b"), "people need to"),
377
+ (re.compile(r"\bbecause of\b", re.I), "due to"),
378
+ (re.compile(r"\bfor example\b", re.I), "for instance"),
379
+ (re.compile(r"\bthink that\b", re.I), "think"),
380
+ (re.compile(r"\bbelieve that\b", re.I), "believe"),
381
+ (re.compile(r"\bby giving\b", re.I), "by offering"),
382
+ (re.compile(r"\bhelp communities\b", re.I), "support communities"),
383
+ )
384
+
385
+ _VISIBLE_TARGET_SIM = 0.85
386
+ _LM_SHIP_MAX_SIM = 0.88
387
+
388
+
389
  def _surface_humanize_variants(text: str, rng: random.Random) -> list[str]:
390
+ """Phrase + light surface variants that change wording."""
391
  out: list[str] = []
392
  base = (text or "").strip()
393
  if not base:
394
  return out
395
 
396
+ out.extend(_apply_phrase_rewrites(base))
397
  out.extend(_esl_structural_variants(base))
398
 
399
+ # Because-clause fronting only when combined with a phrase rewrite later
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  m = re.match(
401
  r"^(?P<main>.+?)\s+because\s+(?P<reason>.+?)(?P<end>[.!?])?$",
402
  base,
 
416
  cand = pat.sub(repl, base, count=1)
417
  if cand.strip() and cand.strip() != base:
418
  out.append(cand.strip())
419
+ # Stack phrase rewrites on surface swaps
420
+ for seed in list(out[:8]):
421
+ out.extend(_apply_phrase_rewrites(seed))
422
+ out.extend(_esl_structural_variants(seed))
 
 
 
 
 
 
 
 
423
  return out
424
 
425
 
 
431
  strength: int = 2,
432
  target_sim: float = _VISIBLE_TARGET_SIM,
433
  ) -> str:
434
+ """Produce a true lexical rewritenot grammar-only or clause shuffle."""
435
  base = scrub_phrases(source_unit)
436
  base = apply_tone_contractions(base, tone)
437
  base = tidy(correct_text(base) if base else "")
 
451
  structured = base
452
 
453
  trials: list[str] = []
454
+ for seed in (base, structured):
455
+ trials.extend(_apply_phrase_rewrites(seed))
456
+ trials.extend(_esl_structural_variants(seed))
457
+ trials.extend(_surface_humanize_variants(seed, rng))
458
  if structured.strip() and structured.strip() != base.strip():
459
  trials.append(structured)
460
 
461
+ # Synonym passes allow stronger lexicon; safety filter keeps meaning
462
+ for syn_src, syn_str, force in (
463
+ (structured, 2, True),
464
+ (base, 2, True),
465
+ (structured, 1, True),
466
+ (base, 1, False),
467
+ ):
468
  try:
469
  cand = rewrite_sentence_synonyms(
470
  syn_src,
471
  syn_str,
472
  rng,
473
  tone=tone,
474
+ force_all_lexicon=force,
475
  )
476
  cand = apply_tone_contractions(cand, tone)
477
  cand = tidy(correct_text(cand) if cand else "")
478
  if cand.strip():
479
  trials.append(cand)
480
+ trials.extend(_apply_phrase_rewrites(cand))
481
  trials.extend(_esl_structural_variants(cand))
482
  except Exception:
483
  continue
 
492
  uniq.append(t.strip())
493
 
494
  best = _ensure_unit_terminal(source_unit)
495
+ best_score = -1.0
496
  for trial in uniq:
497
  ok, sim = _safe_paraphrase_trial(source_unit, trial)
498
  if not ok:
 
500
  t = _ensure_unit_terminal(
501
  _clamp_to_source_shape(source_unit, trial.strip()) or trial.strip()
502
  )
503
+ score = _rewrite_score(source_unit, t)
504
+ if score > best_score or (
505
+ abs(score - best_score) < 1e-9 and sim < _similarity_ratio(source_unit, best)
506
+ ):
507
  best = t
508
+ best_score = score
509
+
510
+ # Final phrase polish on the winner — push more wording change when safe
511
+ if best_score >= 0:
512
+ for polished in _apply_phrase_rewrites(best, max_swaps=4):
513
+ if not _is_true_rewrite(source_unit, polished):
514
+ continue
515
+ t = _ensure_unit_terminal(
516
+ _clamp_to_source_shape(source_unit, polished.strip())
517
+ or polished.strip()
518
+ )
519
+ score = _rewrite_score(source_unit, t)
520
+ if score > best_score:
521
+ best = t
522
+ best_score = score
523
  return best
524
 
525
 
 
530
  *,
531
  strength: int = 0,
532
  ) -> str:
533
+ """Backward-compatible name — now runs true lexical paraphrase."""
534
  return _visible_unit_paraphrase(
535
  source_unit,
536
  tone,
 
549
  target_sim: float = _VISIBLE_TARGET_SIM,
550
  min_words: int = 4,
551
  ) -> str:
552
+ """Never ship grammar-only / reorder-only when a true rewrite exists."""
553
  src = (source_unit or "").strip()
554
  cand = (candidate or src).strip() or src
555
  if len(src.split()) < min_words:
556
  return _ensure_unit_terminal(cand)
557
 
558
+ if _is_true_rewrite(src, cand) and _similarity_ratio(src, cand) <= target_sim:
559
+ return _ensure_unit_terminal(cand)
 
 
 
 
 
 
560
 
561
+ # Classical true rewrite from source (ignore weak LM near-copies)
562
  trials = [
563
  _visible_unit_paraphrase(src, tone, rng, strength=2, target_sim=target_sim),
564
+ _visible_unit_paraphrase(src, tone, rng, strength=2, target_sim=0.78),
565
  ]
 
 
 
 
 
 
 
566
  best = _ensure_unit_terminal(cand)
567
+ best_score = -1.0
568
+ if _is_true_rewrite(src, best):
569
+ best_score = _rewrite_score(src, best)
570
  for trial in trials:
571
+ if not _is_true_rewrite(src, trial):
 
572
  continue
573
  t = _ensure_unit_terminal(
574
  _clamp_to_source_shape(src, trial.strip()) or trial.strip()
575
  )
576
+ score = _rewrite_score(src, t)
577
+ if score > best_score:
 
578
  best = t
579
+ best_score = score
 
 
580
  return best
581
 
582
 
 
588
  *,
589
  threshold: float = _VISIBLE_TARGET_SIM,
590
  ) -> str:
591
+ """If a kept unit is still nearly identical, force a true rewrite."""
592
  return _require_visible_rewrite(
593
  source_unit,
594
  candidate,
 
813
  def _accept_lm_unit(
814
  source_unit: str, candidate: str, *, max_sim: float = _LM_SHIP_MAX_SIM
815
  ) -> tuple[bool, list[str]]:
816
+ """Strict gate for a single LM-rewritten sentence — must be a true rewrite."""
817
  if not candidate or not candidate.strip():
818
  return False, ["empty"]
 
819
  o_n = _sentence_count_simple(source_unit)
820
  c_n = _sentence_count_simple(candidate)
821
  if o_n == 1 and c_n != 1:
822
  return False, ["shape"]
823
  if o_n >= 1 and c_n < o_n:
824
  return False, ["shape"]
825
+ sim = _similarity_ratio(source_unit, candidate)
826
+ if sim >= max_sim or sim >= _LM_SHIP_MAX_SIM:
827
+ return False, ["near_copy"]
828
+ # Must be a true lexical rewrite (not grammar/reorder)
829
+ if not _is_true_rewrite(source_unit, candidate):
830
+ return False, ["near_copy"]
831
  v = validate_candidate(
832
+ source_unit, candidate, min_meaning=0.72, max_surface=max_sim
833
  )
834
+ soft = {
835
+ "too_similar",
836
+ "identical",
837
+ "near_copy",
838
+ "too_divergent",
839
+ "key_content",
840
+ "coverage",
841
+ "drift",
842
+ "grammar_worse",
843
+ "collocation",
844
+ }
845
+ hard_keys = [r.split(":")[0] for r in v.reasons if r.split(":")[0] not in soft]
846
+ if _hard_fail(hard_keys):
847
+ return False, list(v.reasons) or ["reject"]
848
  return True, []
849
 
850
 
scripts/__pycache__/test_phase_a_guards.cpython-311.pyc ADDED
Binary file (24.8 kB). View file
 
scripts/test_phase_a_guards.py CHANGED
@@ -63,29 +63,30 @@ def test_skip_short_and_light() -> None:
63
  def test_sleep_style_must_visibly_rewrite() -> None:
64
  """User expectation: not grammar-only; wording must clearly change."""
65
  import random
 
66
 
67
  from app.pipeline.orchestrator import (
 
68
  _require_visible_rewrite,
69
  _similarity_ratio,
 
70
  )
71
 
72
  rng = random.Random(7)
73
  src = "Another important thing is sleeping."
74
  out = _require_visible_rewrite(src, src, "Neutral", rng, target_sim=0.85)
75
- assert out.lower().rstrip(".!?") != src.lower().rstrip(".!?")
76
  assert _similarity_ratio(src, out) < 0.90
 
77
  assert "sleep" in out.lower()
78
  print("sleep opener rewrite OK:", out)
79
 
80
  src2 = "Without enough sleep, the brain."
81
  out2 = _require_visible_rewrite(src2, src2, "Neutral", rng, target_sim=0.85)
82
- assert out2.lower().rstrip(".!?") != src2.lower().rstrip(".!?")
83
  assert "sleep" in out2.lower()
84
  print("without-enough rewrite OK:", out2)
85
 
86
- # Full paste via hybrid with near-copy LM → must still visibly rewrite
87
- from unittest.mock import patch
88
-
89
  raw = "Another important thing are sleeping. Without enough sleep, the brain"
90
  g = correct_text(raw)
91
  with patch(
@@ -99,13 +100,56 @@ def test_sleep_style_must_visibly_rewrite() -> None:
99
  )
100
  assert _similarity_ratio(g, out) < 0.90
101
  assert "sleep" in out.lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  low = out.lower()
103
- assert "another important thing is sleeping" not in low
 
 
104
  assert not (
105
- low.startswith("another important thing")
106
- and "without enough sleep, the brain" in low
107
  )
108
- print("sleep document hybrid rewrite OK:", out)
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
 
111
  def test_rejects_hollow_ons_bills() -> None:
@@ -419,6 +463,7 @@ if __name__ == "__main__":
419
  test_rejects_two_sentence_for_one()
420
  test_skip_short_and_light()
421
  test_sleep_style_must_visibly_rewrite()
 
422
  test_rejects_hollow_ons_bills()
423
  test_assembly_preserves_unit_count()
424
  test_live_bug_patterns_rejected()
 
63
  def test_sleep_style_must_visibly_rewrite() -> None:
64
  """User expectation: not grammar-only; wording must clearly change."""
65
  import random
66
+ from unittest.mock import patch
67
 
68
  from app.pipeline.orchestrator import (
69
+ _is_true_rewrite,
70
  _require_visible_rewrite,
71
  _similarity_ratio,
72
+ _stem_jaccard,
73
  )
74
 
75
  rng = random.Random(7)
76
  src = "Another important thing is sleeping."
77
  out = _require_visible_rewrite(src, src, "Neutral", rng, target_sim=0.85)
78
+ assert _is_true_rewrite(src, out), out
79
  assert _similarity_ratio(src, out) < 0.90
80
+ assert _stem_jaccard(src, out) < 0.92
81
  assert "sleep" in out.lower()
82
  print("sleep opener rewrite OK:", out)
83
 
84
  src2 = "Without enough sleep, the brain."
85
  out2 = _require_visible_rewrite(src2, src2, "Neutral", rng, target_sim=0.85)
86
+ assert _is_true_rewrite(src2, out2), out2
87
  assert "sleep" in out2.lower()
88
  print("without-enough rewrite OK:", out2)
89
 
 
 
 
90
  raw = "Another important thing are sleeping. Without enough sleep, the brain"
91
  g = correct_text(raw)
92
  with patch(
 
100
  )
101
  assert _similarity_ratio(g, out) < 0.90
102
  assert "sleep" in out.lower()
103
+ # Must not be grammar-only near-copy of the classic paste
104
+ assert "another important thing is sleeping" not in out.lower()
105
+ print("sleep document hybrid rewrite OK:", out)
106
+
107
+
108
+ def test_exercise_tv_must_not_be_reorder_only() -> None:
109
+ """User complaint sample: must change wording, keep regularly/claims."""
110
+ import random
111
+ from unittest.mock import patch
112
+
113
+ from app.pipeline.orchestrator import (
114
+ _hybrid_rewrite,
115
+ _is_true_rewrite,
116
+ _similarity_ratio,
117
+ )
118
+
119
+ raw = (
120
+ "Exercise is one of the most important activity that keeps our body fit. "
121
+ "If a person exercise regularly, they will have less chance to gets diseases. "
122
+ "Unfortunately, many peoples prefers watching television instead of doing "
123
+ "physical activities."
124
+ )
125
+ g = correct_text(raw)
126
+ rng = random.Random(11)
127
+ with patch(
128
+ "app.pipeline.orchestrator.rewrite_unit",
129
+ side_effect=lambda u, **_k: u,
130
+ ):
131
+ out, stats, _ = _hybrid_rewrite(g, "Neutral", 1, rng, can_generate=True)
132
  low = out.lower()
133
+ assert _similarity_ratio(g, out) < 0.90
134
+ # Not the weak template / reorder-only patterns users rejected
135
+ assert "very important part of activities that keeps" not in low
136
  assert not (
137
+ "instead of doing physical activities, unfortunately" in low
138
+ and "watching television" in low
139
  )
140
+ # Claims should survive in some form
141
+ assert "tv" in low or "television" in low
142
+ assert "fit" in low or "exercise" in low or "work out" in low or "workout" in low
143
+ # Spot-check units are true rewrites vs grammar source units
144
+ from app.pipeline.alignment import iter_source_units
145
+
146
+ src_units = [u for _i, u in iter_source_units(g)]
147
+ out_units = [u for _i, u in iter_source_units(out)]
148
+ assert len(out_units) == len(src_units)
149
+ true_n = sum(1 for s, o in zip(src_units, out_units) if _is_true_rewrite(s, o))
150
+ assert true_n >= max(1, len(src_units) - 1), (true_n, out)
151
+ print("exercise/TV rewrite OK:", out)
152
+ print(" stats", stats)
153
 
154
 
155
  def test_rejects_hollow_ons_bills() -> None:
 
463
  test_rejects_two_sentence_for_one()
464
  test_skip_short_and_light()
465
  test_sleep_style_must_visibly_rewrite()
466
+ test_exercise_tv_must_not_be_reorder_only()
467
  test_rejects_hollow_ons_bills()
468
  test_assembly_preserves_unit_count()
469
  test_live_bug_patterns_rejected()