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
|
| 125 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
t = (trial or "").strip()
|
| 127 |
if not t:
|
| 128 |
-
return False
|
| 129 |
t = _clamp_to_source_shape(source_unit, t) or t
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
if _hard_fail(hard_keys):
|
| 137 |
-
return False
|
| 138 |
-
|
|
|
|
|
|
|
| 139 |
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 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 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
|
| 180 |
-
def
|
| 181 |
-
|
|
|
|
| 182 |
if not t:
|
| 183 |
-
return
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
|
| 187 |
-
def
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
|
| 194 |
def _esl_structural_variants(text: str) -> list[str]:
|
| 195 |
-
"""
|
| 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)}
|
| 214 |
-
out.append(f"{
|
| 215 |
-
out.append(f"
|
| 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 |
-
|
| 223 |
-
out.append(f"{
|
| 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
|
| 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)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
return out
|
| 255 |
|
| 256 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
def _surface_humanize_variants(text: str, rng: random.Random) -> list[str]:
|
| 258 |
-
"""
|
| 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 |
-
#
|
| 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 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 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 |
-
"""
|
| 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 |
-
|
| 348 |
-
|
| 349 |
-
|
|
|
|
| 350 |
if structured.strip() and structured.strip() != base.strip():
|
| 351 |
trials.append(structured)
|
| 352 |
|
| 353 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
try:
|
| 355 |
cand = rewrite_sentence_synonyms(
|
| 356 |
syn_src,
|
| 357 |
syn_str,
|
| 358 |
rng,
|
| 359 |
tone=tone,
|
| 360 |
-
force_all_lexicon=
|
| 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(
|
| 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 |
-
|
| 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 |
-
|
| 390 |
-
|
| 391 |
-
|
|
|
|
| 392 |
best = t
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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 |
-
|
| 432 |
-
|
| 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 |
-
#
|
| 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.
|
| 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 |
-
|
|
|
|
|
|
|
| 454 |
for trial in trials:
|
| 455 |
-
|
| 456 |
-
if not ok:
|
| 457 |
continue
|
| 458 |
t = _ensure_unit_terminal(
|
| 459 |
_clamp_to_source_shape(src, trial.strip()) or trial.strip()
|
| 460 |
)
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
if sim < best_sim:
|
| 464 |
best = t
|
| 465 |
-
|
| 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
|
| 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.
|
| 716 |
)
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
|
|
|
|
|
|
|
|
|
| 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 rewrite — not 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
|
| 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
|
| 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
|
|
|
|
|
|
|
| 104 |
assert not (
|
| 105 |
-
|
| 106 |
-
and "
|
| 107 |
)
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|