idnameraj commited on
Commit
fb8ceda
·
verified ·
1 Parent(s): 9fba648

Upload 105 files

Browse files
app/pipeline/__pycache__/candidate_validator.cpython-311.pyc CHANGED
Binary files a/app/pipeline/__pycache__/candidate_validator.cpython-311.pyc and b/app/pipeline/__pycache__/candidate_validator.cpython-311.pyc differ
 
app/pipeline/__pycache__/meaning_safety.cpython-311.pyc CHANGED
Binary files a/app/pipeline/__pycache__/meaning_safety.cpython-311.pyc and b/app/pipeline/__pycache__/meaning_safety.cpython-311.pyc differ
 
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
@@ -367,6 +367,27 @@ def _invention_ok(original: str, candidate: str) -> bool:
367
  if "program" in novel or "programme" in novel:
368
  if novel & {"exercise", "fitness", "gym", "workout", "training"}:
369
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  return True
371
 
372
 
 
367
  if "program" in novel or "programme" in novel:
368
  if novel & {"exercise", "fitness", "gym", "workout", "training"}:
369
  return False
370
+ # Invented "learn/learning to <verb>" when verb not in source (e.g. learning to read)
371
+ for m in re.finditer(
372
+ r"\b(?:learn(?:ing)?|study(?:ing)?)\s+to\s+([a-zA-Z]+)\b",
373
+ (candidate or ""),
374
+ flags=re.I,
375
+ ):
376
+ verb = m.group(1).lower()
377
+ stem = verb
378
+ if stem.endswith("ing") and len(stem) > 5:
379
+ stem = stem[:-3]
380
+ elif stem.endswith("ed") and len(stem) > 4:
381
+ stem = stem[:-2]
382
+ elif stem.endswith("s") and len(stem) > 3:
383
+ stem = stem[:-1]
384
+ if stem not in o_toks and verb not in o_toks:
385
+ return False
386
+ # Invented "check with …" meeting frame not in source
387
+ if re.search(r"\bcheck with\b", candidate or "", flags=re.I) and not re.search(
388
+ r"\bcheck with\b", original or "", flags=re.I
389
+ ):
390
+ return False
391
  return True
392
 
393
 
app/pipeline/meaning_safety.py CHANGED
@@ -164,6 +164,40 @@ def _lemma_still_negated(text: str, lemma: str) -> bool:
164
  return False
165
 
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  def polarity_safe(original: str, candidate: str) -> bool:
168
  """Reject candidates that flip antonyms or strip/add negation on key verbs."""
169
  if not original.strip() or not candidate.strip():
@@ -220,4 +254,7 @@ def polarity_safe(original: str, candidate: str) -> bool:
220
  if not _NEG_EQUIV.search(c):
221
  return False
222
 
 
 
 
223
  return True
 
164
  return False
165
 
166
 
167
+ def _comparison_sides(text: str) -> tuple[set[str], set[str]] | None:
168
+ """Extract content stems on each side of a less/more…than comparison."""
169
+ m = re.search(
170
+ r"^(.{3,80}?)\b(?:less|more|fewer|greater|better|worse)\b"
171
+ r".{0,100}?\bthan\s+(.{3,80}?)(?:[.!?]|$)",
172
+ (text or "").strip(),
173
+ flags=re.I | re.S,
174
+ )
175
+ if not m:
176
+ return None
177
+ left = _stems_in_text(m.group(1))
178
+ right = _stems_in_text(m.group(2))
179
+ if not left or not right:
180
+ return None
181
+ return left, right
182
+
183
+
184
+ def _comparison_order_ok(original: str, candidate: str) -> bool:
185
+ """Reject swapped comparison subjects (Electric…than petrol → Petrol…than electric)."""
186
+ sides_o = _comparison_sides(original)
187
+ sides_c = _comparison_sides(candidate)
188
+ if not sides_o or not sides_c:
189
+ return True
190
+ lo, ro = sides_o
191
+ lc, rc = sides_c
192
+ lo_rc = len(lo & rc) / max(1, len(lo))
193
+ lo_lc = len(lo & lc) / max(1, len(lo))
194
+ ro_lc = len(ro & lc) / max(1, len(ro))
195
+ # Original left aligns with candidate right, and original right with candidate left
196
+ if lo_rc >= 0.45 and ro_lc >= 0.45 and lo_lc < 0.45:
197
+ return False
198
+ return True
199
+
200
+
201
  def polarity_safe(original: str, candidate: str) -> bool:
202
  """Reject candidates that flip antonyms or strip/add negation on key verbs."""
203
  if not original.strip() or not candidate.strip():
 
254
  if not _NEG_EQUIV.search(c):
255
  return False
256
 
257
+ if not _comparison_order_ok(original, candidate):
258
+ return False
259
+
260
  return True
app/pipeline/orchestrator.py CHANGED
@@ -185,6 +185,24 @@ def _sentence_count_simple(text: str) -> int:
185
  return len([p for p in parts if p.strip()])
186
 
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  def _classical_paragraph(paragraph: str, tone: str, strength: int, rng: random.Random) -> str:
189
  """Offline fallback: structure + curated lexicon (not the primary quality path)."""
190
  styled = apply_tone_style(paragraph, tone, strength, rng)
@@ -357,6 +375,13 @@ def _accept_lm_unit(source_unit: str, candidate: str, *, max_sim: float = 0.90)
357
  """Strict gate for a single LM-rewritten sentence."""
358
  if not candidate or not candidate.strip():
359
  return False, ["empty"]
 
 
 
 
 
 
 
360
  v = validate_candidate(
361
  source_unit, candidate, min_meaning=0.80, max_surface=max_sim
362
  )
@@ -463,9 +488,7 @@ def _hybrid_rewrite(
463
  max_sim=0.90,
464
  )
465
  gen_clean = tidy(correct_text(gen_raw) if gen_raw else "")
466
- # Reject multi-sentence LM output for a single source unit
467
- if _sentence_count_simple(gen_clean) > 1 and _sentence_count_simple(src_u) == 1:
468
- gen_clean = ""
469
  ok, reasons = _accept_lm_unit(src_u, gen_clean, max_sim=0.90)
470
 
471
  if ok:
@@ -482,11 +505,7 @@ def _hybrid_rewrite(
482
  max_sim=0.88,
483
  )
484
  gen_clean2 = tidy(correct_text(gen_raw2) if gen_raw2 else "")
485
- if (
486
- _sentence_count_simple(gen_clean2) > 1
487
- and _sentence_count_simple(src_u) == 1
488
- ):
489
- gen_clean2 = ""
490
  ok2, reasons2 = _accept_lm_unit(src_u, gen_clean2, max_sim=0.88)
491
  if ok2:
492
  stats.gen_accepted += 1
@@ -512,9 +531,13 @@ def _hybrid_rewrite(
512
 
513
  if not (chosen or "").strip():
514
  chosen = src_u
515
- # One source unit one sentence max in the slot
516
- if _sentence_count_simple(chosen) > 1 and _sentence_count_simple(src_u) == 1:
517
- chosen = _light_revert_polish(src_u, tone, rng, strength=1)
 
 
 
 
518
  outputs.append(_ensure_unit_terminal(chosen))
519
 
520
  if len(outputs) != len(source_units):
@@ -527,8 +550,21 @@ def _hybrid_rewrite(
527
  outputs.append(_ensure_unit_terminal(source_units[len(outputs)][1]))
528
  outputs = outputs[: len(source_units)]
529
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  # Assemble ONLY from per-unit strings — no document-level grammar here
531
- # (document LT was observed dropping sentences, e.g. Reading sample).
532
  text = _assemble_hybrid_units(source_units, outputs)
533
  scrubbed = tidy(_light_post_gen(text, tone))
534
  if len(iter_source_units(scrubbed)) < len(source_units):
@@ -540,9 +576,15 @@ def _hybrid_rewrite(
540
  text = _assemble_hybrid_units(source_units, outputs)
541
  else:
542
  text = scrubbed
543
- # Final hard guarantee
544
  if len(iter_source_units(text)) < len(source_units):
 
545
  text = _assemble_hybrid_units(source_units, outputs)
 
 
 
 
 
546
  return text, stats, used_gen
547
 
548
 
@@ -760,6 +802,26 @@ def rewrite_text(
760
  else:
761
  rewritten = tidy(cleaned_out)
762
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
  ratio = _similarity_ratio(source, rewritten)
764
  changed = rewritten.strip() != original.strip()
765
 
 
185
  return len([p for p in parts if p.strip()])
186
 
187
 
188
+ def _clamp_to_source_shape(source_unit: str, candidate: str) -> str:
189
+ """Keep LM output to the same sentence count as the source unit (1→1)."""
190
+ c = (candidate or "").strip()
191
+ if not c:
192
+ return ""
193
+ o_n = _sentence_count_simple(source_unit)
194
+ c_n = _sentence_count_simple(c)
195
+ if o_n == 1 and c_n != 1:
196
+ # Take first sentence only; if still multi or empty, reject
197
+ first = re.split(r"(?<=[.!?])\s+", c, maxsplit=1)[0].strip()
198
+ if _sentence_count_simple(first) == 1 and first:
199
+ return first
200
+ return ""
201
+ if o_n >= 1 and c_n < o_n:
202
+ return ""
203
+ return c
204
+
205
+
206
  def _classical_paragraph(paragraph: str, tone: str, strength: int, rng: random.Random) -> str:
207
  """Offline fallback: structure + curated lexicon (not the primary quality path)."""
208
  styled = apply_tone_style(paragraph, tone, strength, rng)
 
375
  """Strict gate for a single LM-rewritten sentence."""
376
  if not candidate or not candidate.strip():
377
  return False, ["empty"]
378
+ # Hard 1:1 sentence shape before other gates
379
+ o_n = _sentence_count_simple(source_unit)
380
+ c_n = _sentence_count_simple(candidate)
381
+ if o_n == 1 and c_n != 1:
382
+ return False, ["shape"]
383
+ if o_n >= 1 and c_n < o_n:
384
+ return False, ["shape"]
385
  v = validate_candidate(
386
  source_unit, candidate, min_meaning=0.80, max_surface=max_sim
387
  )
 
488
  max_sim=0.90,
489
  )
490
  gen_clean = tidy(correct_text(gen_raw) if gen_raw else "")
491
+ gen_clean = _clamp_to_source_shape(src_u, gen_clean)
 
 
492
  ok, reasons = _accept_lm_unit(src_u, gen_clean, max_sim=0.90)
493
 
494
  if ok:
 
505
  max_sim=0.88,
506
  )
507
  gen_clean2 = tidy(correct_text(gen_raw2) if gen_raw2 else "")
508
+ gen_clean2 = _clamp_to_source_shape(src_u, gen_clean2)
 
 
 
 
509
  ok2, reasons2 = _accept_lm_unit(src_u, gen_clean2, max_sim=0.88)
510
  if ok2:
511
  stats.gen_accepted += 1
 
531
 
532
  if not (chosen or "").strip():
533
  chosen = src_u
534
+ chosen = _clamp_to_source_shape(src_u, chosen) or src_u
535
+ # Never let an accepted unit expand into multiple sentences
536
+ if _sentence_count_simple(chosen) != _sentence_count_simple(src_u):
537
+ if _sentence_count_simple(src_u) == 1:
538
+ chosen = _light_revert_polish(src_u, tone, rng, strength=1)
539
+ else:
540
+ chosen = src_u
541
  outputs.append(_ensure_unit_terminal(chosen))
542
 
543
  if len(outputs) != len(source_units):
 
550
  outputs.append(_ensure_unit_terminal(source_units[len(outputs)][1]))
551
  outputs = outputs[: len(source_units)]
552
 
553
+ # Per-slot sanity: empty/too-short slots → source unit
554
+ fixed_outs: list[str] = []
555
+ for (_pi, src_u), out in zip(source_units, outputs):
556
+ piece = (out or "").strip()
557
+ if (
558
+ not piece
559
+ or _sentence_count_simple(piece) < _sentence_count_simple(src_u)
560
+ or len(piece.split()) < max(3, int(len(src_u.split()) * 0.55))
561
+ ):
562
+ piece = _ensure_unit_terminal(src_u)
563
+ stats.bump_reason("unit_restore")
564
+ fixed_outs.append(_ensure_unit_terminal(piece))
565
+ outputs = fixed_outs
566
+
567
  # Assemble ONLY from per-unit strings — no document-level grammar here
 
568
  text = _assemble_hybrid_units(source_units, outputs)
569
  scrubbed = tidy(_light_post_gen(text, tone))
570
  if len(iter_source_units(scrubbed)) < len(source_units):
 
576
  text = _assemble_hybrid_units(source_units, outputs)
577
  else:
578
  text = scrubbed
579
+ # Final hard guarantee — never ship fewer units than source
580
  if len(iter_source_units(text)) < len(source_units):
581
+ logger.warning("Final hybrid text lost units; restoring assembled outputs")
582
  text = _assemble_hybrid_units(source_units, outputs)
583
+ if len(iter_source_units(text)) < len(source_units):
584
+ text = _assemble_hybrid_units(
585
+ source_units,
586
+ [_ensure_unit_terminal(u) for _i, u in source_units],
587
+ )
588
  return text, stats, used_gen
589
 
590
 
 
802
  else:
803
  rewritten = tidy(cleaned_out)
804
 
805
+ # Nuclear hybrid safety: never return fewer sentence units than the source
806
+ if mode == "hybrid" and hybrid_stats and hybrid_stats.units:
807
+ n_src = hybrid_stats.units
808
+ if len(iter_source_units(rewritten)) < n_src:
809
+ logger.warning(
810
+ "Post-pipeline unit loss (%s→%s); restoring from grammar source units",
811
+ n_src,
812
+ len(iter_source_units(rewritten)),
813
+ )
814
+ src_units = iter_source_units(source)
815
+ rewritten = _assemble_hybrid_units(
816
+ src_units,
817
+ [
818
+ _ensure_unit_terminal(
819
+ _light_revert_polish(u, tone, rng, strength=1)
820
+ )
821
+ for _i, u in src_units
822
+ ],
823
+ )
824
+
825
  ratio = _similarity_ratio(source, rewritten)
826
  changed = rewritten.strip() != original.strip()
827
 
scripts/test_phase_a_guards.py CHANGED
@@ -270,6 +270,64 @@ def test_assemble_preserves_two_units() -> None:
270
  print("assemble two units OK:", text)
271
 
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  if __name__ == "__main__":
274
  test_rejects_multi_sentence_invention()
275
  test_rejects_two_sentence_for_one()
@@ -288,4 +346,8 @@ if __name__ == "__main__":
288
  test_rejects_garbage_dontstet()
289
  test_rejects_recycle_negation_flip()
290
  test_assemble_preserves_two_units()
 
 
 
 
291
  print("\nALL PHASE A TESTS PASSED")
 
270
  print("assemble two units OK:", text)
271
 
272
 
273
+ def test_rejects_comparison_swap() -> None:
274
+ src = "Electric cars produce less air pollution than petrol cars."
275
+ bad = "Petrol cars make less air pollution than electric cars."
276
+ from app.pipeline.meaning_safety import polarity_safe
277
+
278
+ assert not polarity_safe(src, bad)
279
+ v = validate_candidate(src, bad, min_meaning=0.5)
280
+ assert not v.ok
281
+ assert "polarity" in v.reasons
282
+ ok, reasons = _accept_lm_unit(src, bad)
283
+ assert not ok
284
+ print("comparison swap reject OK:", v.reasons, reasons)
285
+
286
+
287
+ def test_rejects_learning_to_read_invention() -> None:
288
+ src = "Learning a second language opens more career opportunities."
289
+ bad = "Learning to read a second language opens many career opportunities."
290
+ v = validate_candidate(src, bad, min_meaning=0.5)
291
+ assert not v.ok
292
+ assert "invention" in v.reasons
293
+ print("learning-to-read invention reject OK:", v.reasons)
294
+
295
+
296
+ def test_rejects_check_with_teachers_invention() -> None:
297
+ src = (
298
+ "Teachers should check progress regularly instead of grading only "
299
+ "the final report."
300
+ )
301
+ bad = (
302
+ "Teachers should check with teachers about progress instead of "
303
+ "grading the final report."
304
+ )
305
+ v = validate_candidate(src, bad, min_meaning=0.5)
306
+ assert not v.ok
307
+ assert "invention" in v.reasons
308
+ print("check-with-teachers invention reject OK:", v.reasons)
309
+
310
+
311
+ def test_clamp_rejects_multi_sentence() -> None:
312
+ from app.pipeline.orchestrator import (
313
+ _accept_lm_unit,
314
+ _clamp_to_source_shape,
315
+ _sentence_count_simple,
316
+ )
317
+
318
+ src = "Learning a second language opens more career opportunities."
319
+ bad = (
320
+ "Learning a second language opens more career opportunities. "
321
+ "Travel becomes easier too."
322
+ )
323
+ clamped = _clamp_to_source_shape(src, bad)
324
+ assert _sentence_count_simple(clamped) == 1
325
+ ok, reasons = _accept_lm_unit(src, bad)
326
+ assert not ok
327
+ assert "shape" in reasons
328
+ print("multi-sentence clamp/reject OK:", reasons)
329
+
330
+
331
  if __name__ == "__main__":
332
  test_rejects_multi_sentence_invention()
333
  test_rejects_two_sentence_for_one()
 
346
  test_rejects_garbage_dontstet()
347
  test_rejects_recycle_negation_flip()
348
  test_assemble_preserves_two_units()
349
+ test_rejects_comparison_swap()
350
+ test_rejects_learning_to_read_invention()
351
+ test_rejects_check_with_teachers_invention()
352
+ test_clamp_rejects_multi_sentence()
353
  print("\nALL PHASE A TESTS PASSED")