Upload 100 files
Browse files- app/pipeline/__pycache__/candidate_validator.cpython-311.pyc +0 -0
- app/pipeline/__pycache__/generative.cpython-311.pyc +0 -0
- app/pipeline/__pycache__/orchestrator.cpython-311.pyc +0 -0
- app/pipeline/candidate_validator.py +66 -1
- app/pipeline/generative.py +1 -0
- app/pipeline/orchestrator.py +1 -1
- scripts/test_quality_pipeline.py +16 -0
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__/generative.cpython-311.pyc
CHANGED
|
Binary files a/app/pipeline/__pycache__/generative.cpython-311.pyc and b/app/pipeline/__pycache__/generative.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
|
@@ -63,16 +63,78 @@ def _extract_propers(text: str) -> set[str]:
|
|
| 63 |
return found
|
| 64 |
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
def _length_ok(original: str, candidate: str) -> bool:
|
| 67 |
ow = max(1, len(original.split()))
|
| 68 |
cw = len(candidate.split())
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
return False
|
| 71 |
if cw > int(ow * 2.2) + 8:
|
| 72 |
return False
|
| 73 |
return True
|
| 74 |
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
def _meaning_score(original: str, candidate: str) -> float | None:
|
| 77 |
"""MiniLM cosine if available; None if model missing."""
|
| 78 |
try:
|
|
@@ -112,6 +174,9 @@ def validate_candidate(
|
|
| 112 |
if not _length_ok(o, c):
|
| 113 |
reasons.append("length")
|
| 114 |
|
|
|
|
|
|
|
|
|
|
| 115 |
surf = _surface_sim(o, c)
|
| 116 |
if surf >= max_surface:
|
| 117 |
reasons.append("too_similar")
|
|
|
|
| 63 |
return found
|
| 64 |
|
| 65 |
|
| 66 |
+
_STOP = frozenset(
|
| 67 |
+
"""
|
| 68 |
+
a an the and or but if in on at to for of as by with from into over after
|
| 69 |
+
before about than then so too very more most such that this these those
|
| 70 |
+
it its they them their we our you your he she his her is are was were be
|
| 71 |
+
been being have has had do does did will would can could should may might
|
| 72 |
+
not no nor also just only own same other another each every both few many
|
| 73 |
+
much some any all because while when where how what which who whom
|
| 74 |
+
""".split()
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _content_tokens(text: str) -> set[str]:
|
| 79 |
+
"""Lowercased content words (length ≥4) used for coverage checks."""
|
| 80 |
+
toks = re.findall(r"[a-zA-Z']+", text.lower())
|
| 81 |
+
out: set[str] = set()
|
| 82 |
+
for t in toks:
|
| 83 |
+
t = t.strip("'")
|
| 84 |
+
if len(t) < 4 or t in _STOP:
|
| 85 |
+
continue
|
| 86 |
+
# Light stem: drop trailing s/ed/ing for overlap
|
| 87 |
+
stem = t
|
| 88 |
+
if t.endswith("ing") and len(t) > 5:
|
| 89 |
+
stem = t[:-3]
|
| 90 |
+
elif t.endswith("ed") and len(t) > 4:
|
| 91 |
+
stem = t[:-2]
|
| 92 |
+
elif t.endswith("es") and len(t) > 4:
|
| 93 |
+
stem = t[:-2]
|
| 94 |
+
elif t.endswith("s") and len(t) > 4:
|
| 95 |
+
stem = t[:-1]
|
| 96 |
+
out.add(stem)
|
| 97 |
+
return out
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _sentence_count(text: str) -> int:
|
| 101 |
+
parts = re.split(r"[.!?]+", (text or "").strip())
|
| 102 |
+
return len([p for p in parts if p.strip()])
|
| 103 |
+
|
| 104 |
+
|
| 105 |
def _length_ok(original: str, candidate: str) -> bool:
|
| 106 |
ow = max(1, len(original.split()))
|
| 107 |
cw = len(candidate.split())
|
| 108 |
+
# Reject half-dropped paraphrases (FLAN often keeps only the last sentence).
|
| 109 |
+
if ow <= 80:
|
| 110 |
+
min_words = max(3, int(ow * 0.72))
|
| 111 |
+
else:
|
| 112 |
+
min_words = max(3, int(ow * 0.55))
|
| 113 |
+
if cw < min_words:
|
| 114 |
return False
|
| 115 |
if cw > int(ow * 2.2) + 8:
|
| 116 |
return False
|
| 117 |
return True
|
| 118 |
|
| 119 |
|
| 120 |
+
def _coverage_ok(original: str, candidate: str) -> bool:
|
| 121 |
+
"""Reject dropped sentences / severe content loss; allow normal paraphrases."""
|
| 122 |
+
o_sents = _sentence_count(original)
|
| 123 |
+
c_sents = _sentence_count(candidate)
|
| 124 |
+
# Short multi-sentence passages must keep every sentence (anti half-drop).
|
| 125 |
+
if 2 <= o_sents <= 5 and c_sents < o_sents:
|
| 126 |
+
return False
|
| 127 |
+
|
| 128 |
+
o_toks = _content_tokens(original)
|
| 129 |
+
if len(o_toks) >= 5:
|
| 130 |
+
c_toks = _content_tokens(candidate)
|
| 131 |
+
overlap = len(o_toks & c_toks) / max(1, len(o_toks))
|
| 132 |
+
# Only fail on severe stem loss (paraphrases often rewrite many content words).
|
| 133 |
+
if overlap < 0.32:
|
| 134 |
+
return False
|
| 135 |
+
return True
|
| 136 |
+
|
| 137 |
+
|
| 138 |
def _meaning_score(original: str, candidate: str) -> float | None:
|
| 139 |
"""MiniLM cosine if available; None if model missing."""
|
| 140 |
try:
|
|
|
|
| 174 |
if not _length_ok(o, c):
|
| 175 |
reasons.append("length")
|
| 176 |
|
| 177 |
+
if not _coverage_ok(o, c):
|
| 178 |
+
reasons.append("coverage")
|
| 179 |
+
|
| 180 |
surf = _surface_sim(o, c)
|
| 181 |
if surf >= max_surface:
|
| 182 |
reasons.append("too_similar")
|
app/pipeline/generative.py
CHANGED
|
@@ -35,6 +35,7 @@ _failed = False
|
|
| 35 |
_backend: str | None = None
|
| 36 |
|
| 37 |
_SHARED_RULES = (
|
|
|
|
| 38 |
"Use different wording and vary sentence structure — do not copy phrases "
|
| 39 |
"from the input almost verbatim.\n"
|
| 40 |
"Keep every claim and the same polarity. Do not flip negatives or antonyms.\n"
|
|
|
|
| 35 |
_backend: str | None = None
|
| 36 |
|
| 37 |
_SHARED_RULES = (
|
| 38 |
+
"Keep ALL sentences and claims from the paragraph — do not drop or merge away ideas.\n"
|
| 39 |
"Use different wording and vary sentence structure — do not copy phrases "
|
| 40 |
"from the input almost verbatim.\n"
|
| 41 |
"Keep every claim and the same polarity. Do not flip negatives or antonyms.\n"
|
app/pipeline/orchestrator.py
CHANGED
|
@@ -146,7 +146,7 @@ def _accept_generative(source: str, draft: str) -> bool:
|
|
| 146 |
|
| 147 |
def _hard_fail(reasons: list[str]) -> bool:
|
| 148 |
for r in reasons:
|
| 149 |
-
if r
|
| 150 |
return True
|
| 151 |
if r.startswith("meaning") or r.startswith("entity"):
|
| 152 |
return True
|
|
|
|
| 146 |
|
| 147 |
def _hard_fail(reasons: list[str]) -> bool:
|
| 148 |
for r in reasons:
|
| 149 |
+
if r in {"polarity", "numbers", "quotes", "length", "coverage"}:
|
| 150 |
return True
|
| 151 |
if r.startswith("meaning") or r.startswith("entity"):
|
| 152 |
return True
|
scripts/test_quality_pipeline.py
CHANGED
|
@@ -31,6 +31,21 @@ def test_polarity() -> None:
|
|
| 31 |
print("polarity OK")
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
def test_validator_rejects_flips() -> None:
|
| 35 |
orig = (
|
| 36 |
"Nowadays many people are living an unhealthy life because they don't have enough time. "
|
|
@@ -151,6 +166,7 @@ def test_classical_fallback() -> None:
|
|
| 151 |
if __name__ == "__main__":
|
| 152 |
test_polarity()
|
| 153 |
test_validator_rejects_flips()
|
|
|
|
| 154 |
test_validator_rejects_near_copy()
|
| 155 |
test_ranker_rejects_near_copy_prefers_paraphrase()
|
| 156 |
test_ranker_prefers_faithful()
|
|
|
|
| 31 |
print("polarity OK")
|
| 32 |
|
| 33 |
|
| 34 |
+
def test_validator_rejects_truncated() -> None:
|
| 35 |
+
orig = (
|
| 36 |
+
"Nowadays many people are living an unhealthy life because they don't have enough time. "
|
| 37 |
+
"Eating fast foods is becoming very common and people don't realize how much it affects their health."
|
| 38 |
+
)
|
| 39 |
+
truncated = (
|
| 40 |
+
"Fast food is becoming very common because many people don't realize "
|
| 41 |
+
"how much it affects their health."
|
| 42 |
+
)
|
| 43 |
+
v = validate_candidate(orig, truncated, min_meaning=0.5)
|
| 44 |
+
assert not v.ok
|
| 45 |
+
assert "length" in v.reasons or "coverage" in v.reasons
|
| 46 |
+
print("validator rejects truncated OK:", v.reasons)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
def test_validator_rejects_flips() -> None:
|
| 50 |
orig = (
|
| 51 |
"Nowadays many people are living an unhealthy life because they don't have enough time. "
|
|
|
|
| 166 |
if __name__ == "__main__":
|
| 167 |
test_polarity()
|
| 168 |
test_validator_rejects_flips()
|
| 169 |
+
test_validator_rejects_truncated()
|
| 170 |
test_validator_rejects_near_copy()
|
| 171 |
test_ranker_rejects_near_copy_prefers_paraphrase()
|
| 172 |
test_ranker_prefers_faithful()
|