Spaces:
Paused
Paused
Upload 4 files
Browse files- clarity.py +7 -1
- groqai.py +14 -4
- humanizer.py +23 -12
- lexsub.py +22 -6
clarity.py
CHANGED
|
@@ -255,8 +255,14 @@ def assess(pairs, use_meaning=True):
|
|
| 255 |
live = [k for k in live if out[k] is None]
|
| 256 |
if live and use_meaning:
|
| 257 |
sims = MEANING.similarities([pairs[k] for k in live])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
for k, sm in zip(live, sims):
|
| 259 |
-
if sm < MEANING.min_for(pairs[k][0], MEANING.SOFT_SIM):
|
| 260 |
out[k] = "meaning changed"
|
| 261 |
STATS["meaning"] += 1
|
| 262 |
return out
|
|
|
|
| 255 |
live = [k for k in live if out[k] is None]
|
| 256 |
if live and use_meaning:
|
| 257 |
sims = MEANING.similarities([pairs[k] for k in live])
|
| 258 |
+
try: # high vocabulary-change setting: a little more distance
|
| 259 |
+
import humanizer as _H
|
| 260 |
+
t = float(_H.VOCAB_TARGET.get() or 0)
|
| 261 |
+
relax = 0.05 if t >= 50 else 0.03 if t >= 35 else 0.0
|
| 262 |
+
except Exception:
|
| 263 |
+
relax = 0.0
|
| 264 |
for k, sm in zip(live, sims):
|
| 265 |
+
if sm < MEANING.min_for(pairs[k][0], MEANING.SOFT_SIM) - relax:
|
| 266 |
out[k] = "meaning changed"
|
| 267 |
STATS["meaning"] += 1
|
| 268 |
return out
|
groqai.py
CHANGED
|
@@ -684,7 +684,10 @@ for the meaning). For every item write {{n}} alternative versions that:
|
|
| 684 |
Answer only with JSON: {"items": [{"id": 1, "versions": ["...", "..."]}, ...]} — one per id."""
|
| 685 |
|
| 686 |
|
| 687 |
-
|
|
|
|
|
|
|
|
|
|
| 688 |
sents = [t for t, _ in items]
|
| 689 |
terms = _surface_terms(" ".join(sents), glossary)
|
| 690 |
lines = []
|
|
@@ -694,6 +697,11 @@ def _variants_batch(items, glossary, n):
|
|
| 694 |
else:
|
| 695 |
lines.append(f"{i}. SENTENCE: {t}")
|
| 696 |
user = "PROTECTED TERMS: " + ("; ".join(terms) if terms else "(none)") + "\n\n" + "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
max_tokens = min(8000, 500 + int(sum(len(t) for t in sents) * 0.6 * (n + 1)))
|
| 698 |
content = _chat([{"role": "system", "content": VARIANTS_SYSTEM.replace("{n}", str(n))},
|
| 699 |
{"role": "user", "content": user}], 0.7, max_tokens)
|
|
@@ -718,8 +726,9 @@ def _variants_batch(items, glossary, n):
|
|
| 718 |
return out
|
| 719 |
|
| 720 |
|
| 721 |
-
def variants(sentences, glossary=(), originals=None, n=3, progress=None, lo=0.8, hi=0.85):
|
| 722 |
-
"""Up to n correct, human-sounding alternatives per sentence (list, or None if its batch failed).
|
|
|
|
| 723 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 724 |
N = len(sentences)
|
| 725 |
out = [None] * N
|
|
@@ -733,7 +742,8 @@ def variants(sentences, glossary=(), originals=None, n=3, progress=None, lo=0.8,
|
|
| 733 |
progress(lo, desc=f"Finding natural-sounding correct versions of {N} sentence(s)")
|
| 734 |
with ThreadPoolExecutor(max_workers=min(_par(), len(starts))) as pool:
|
| 735 |
futs = {pool.submit(_in_provider, PROVIDER.get(), _variants_batch,
|
| 736 |
-
list(zip(sentences[st:st + size], originals[st:st + size])), glossary, n
|
|
|
|
| 737 |
for st in starts}
|
| 738 |
for f in as_completed(futs):
|
| 739 |
st = futs[f]
|
|
|
|
| 684 |
Answer only with JSON: {"items": [{"id": 1, "versions": ["...", "..."]}, ...]} — one per id."""
|
| 685 |
|
| 686 |
|
| 687 |
+
MIN_CHANGE = _cv.ContextVar("min_change", default=0) # vocabulary-change target (%) for alternatives
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
def _variants_batch(items, glossary, n, min_change=0):
|
| 691 |
sents = [t for t, _ in items]
|
| 692 |
terms = _surface_terms(" ".join(sents), glossary)
|
| 693 |
lines = []
|
|
|
|
| 697 |
else:
|
| 698 |
lines.append(f"{i}. SENTENCE: {t}")
|
| 699 |
user = "PROTECTED TERMS: " + ("; ".join(terms) if terms else "(none)") + "\n\n" + "\n".join(lines)
|
| 700 |
+
if min_change:
|
| 701 |
+
user += (f"\n\nVOCABULARY CHANGE: in every version, at least {int(min_change)}% of the words must differ "
|
| 702 |
+
"from the sentence: use different (correct, common) vocabulary and change the clause order or "
|
| 703 |
+
"voice where natural. Protected terms, numbers and citations stay exactly as written, and every "
|
| 704 |
+
"rule above still holds.")
|
| 705 |
max_tokens = min(8000, 500 + int(sum(len(t) for t in sents) * 0.6 * (n + 1)))
|
| 706 |
content = _chat([{"role": "system", "content": VARIANTS_SYSTEM.replace("{n}", str(n))},
|
| 707 |
{"role": "user", "content": user}], 0.7, max_tokens)
|
|
|
|
| 726 |
return out
|
| 727 |
|
| 728 |
|
| 729 |
+
def variants(sentences, glossary=(), originals=None, n=3, progress=None, lo=0.8, hi=0.85, min_change=0):
|
| 730 |
+
"""Up to n correct, human-sounding alternatives per sentence (list, or None if its batch failed).
|
| 731 |
+
min_change: at least this share (%) of the words should differ (vocabulary-change setting)."""
|
| 732 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 733 |
N = len(sentences)
|
| 734 |
out = [None] * N
|
|
|
|
| 742 |
progress(lo, desc=f"Finding natural-sounding correct versions of {N} sentence(s)")
|
| 743 |
with ThreadPoolExecutor(max_workers=min(_par(), len(starts))) as pool:
|
| 744 |
futs = {pool.submit(_in_provider, PROVIDER.get(), _variants_batch,
|
| 745 |
+
list(zip(sentences[st:st + size], originals[st:st + size])), glossary, n,
|
| 746 |
+
min_change): st
|
| 747 |
for st in starts}
|
| 748 |
for f in as_completed(futs):
|
| 749 |
st = futs[f]
|
humanizer.py
CHANGED
|
@@ -403,8 +403,12 @@ VOCAB_TARGET = contextvars.ContextVar("vocab_target", default=None)
|
|
| 403 |
|
| 404 |
|
| 405 |
def change_target():
|
|
|
|
|
|
|
| 406 |
v = VOCAB_TARGET.get()
|
| 407 |
-
|
|
|
|
|
|
|
| 408 |
FULL_AI = contextvars.ContextVar("full_ai", default=False)
|
| 409 |
# Kept for compatibility with smart.py / app.py versions that set it per pass. The 100% AI mode is now
|
| 410 |
# "5 Parrot versions + Groq check" (Parrot does all rewriting), so this switch has no effect.
|
|
@@ -470,7 +474,7 @@ def _paraphrase_local(texts, temperature, progress=None, lo=0.15, hi=0.85, mode=
|
|
| 470 |
|
| 471 |
full_ai = FULL_AI.get()
|
| 472 |
k = CANDIDATES_AI if full_ai else CANDIDATES
|
| 473 |
-
target = change_target() if full_ai else min(
|
| 474 |
er = EXTRA_ROUNDS.get()
|
| 475 |
rounds = 1 + ((AI_ROUNDS if er is None else er) if (full_ai or target) else 0)
|
| 476 |
temp0 = min(1.5, float(temperature) + (0.2 if full_ai else 0.0))
|
|
@@ -548,8 +552,11 @@ def _paraphrase_local(texts, temperature, progress=None, lo=0.15, hi=0.85, mode=
|
|
| 548 |
extra_rounds = meaning_dropped = 0
|
| 549 |
qstat = dict(n=0, meaning=0, fluency=0, second=0, sims=[])
|
| 550 |
ref = [anchor_of(s) or s for s, _, _ in sent_map] # the author's original sentence
|
| 551 |
-
|
| 552 |
-
|
|
|
|
|
|
|
|
|
|
| 553 |
# correct sentences with replaced vocabulary: suitable in-context synonyms, structure unchanged (always available
|
| 554 |
# as a clear option, so a rejected paraphrase is replaced by new wording instead of the original sentence)
|
| 555 |
lex = [[] for _ in sent_map]
|
|
@@ -557,7 +564,7 @@ def _paraphrase_local(texts, temperature, progress=None, lo=0.15, hi=0.85, mode=
|
|
| 557 |
if progress is not None:
|
| 558 |
progress(lo, desc="Finding suitable replacement vocabulary")
|
| 559 |
try:
|
| 560 |
-
lex = LX.variants(ref, glossary, n=LEX_N)
|
| 561 |
except Exception as err:
|
| 562 |
print(f"[humanizer] vocabulary replacement skipped: {err}", flush=True)
|
| 563 |
print(f"[humanizer] vocabulary replacement: {sum(1 for x in lex if x)} of {len(lex)} sentences got "
|
|
@@ -742,15 +749,16 @@ def _paraphrase_local(texts, temperature, progress=None, lo=0.15, hi=0.85, mode=
|
|
| 742 |
cur = [chosen[si] or sent_map[si][0] for si in want]
|
| 743 |
sc = _detector_scores(cur) or [1.0] * len(cur)
|
| 744 |
now = dict(zip(want, sc))
|
| 745 |
-
|
| 746 |
-
|
|
|
|
| 747 |
if todo:
|
| 748 |
vs = None
|
| 749 |
for prov in [G.PROVIDER.get()] + (["openai"] if G.PROVIDER.get() != "openai" and G.openai_available()
|
| 750 |
else []):
|
| 751 |
try:
|
| 752 |
vs = G._in_provider(prov, G.variants, [ref[si] for si in todo], glossary, None, RESCUE_N,
|
| 753 |
-
progress, hi, hi)
|
| 754 |
break
|
| 755 |
except G.Unavailable as err:
|
| 756 |
print(f"[humanizer] rescue rewrites unavailable ({prov}): {err}", flush=True)
|
|
@@ -775,10 +783,13 @@ def _paraphrase_local(texts, temperature, progress=None, lo=0.15, hi=0.85, mode=
|
|
| 775 |
csc = _detector_scores([v for _, v in cands]) or [0.5] * len(cands)
|
| 776 |
best = {}
|
| 777 |
for (si, v), d in zip(cands, csc):
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
|
|
|
|
|
|
|
|
|
| 782 |
chosen[si] = v
|
| 783 |
rescued += 1
|
| 784 |
CL.report()
|
|
|
|
| 403 |
|
| 404 |
|
| 405 |
def change_target():
|
| 406 |
+
"""New-wording target per sentence: the customer's Vocabulary change setting when they chose one (up to 90%),
|
| 407 |
+
otherwise the automatic level (capped at HUM_MAX_VOCAB)."""
|
| 408 |
v = VOCAB_TARGET.get()
|
| 409 |
+
if v:
|
| 410 |
+
return min(90.0, float(v))
|
| 411 |
+
return min(MAX_VOCAB, float(AI_CHANGE_TARGET))
|
| 412 |
FULL_AI = contextvars.ContextVar("full_ai", default=False)
|
| 413 |
# Kept for compatibility with smart.py / app.py versions that set it per pass. The 100% AI mode is now
|
| 414 |
# "5 Parrot versions + Groq check" (Parrot does all rewriting), so this switch has no effect.
|
|
|
|
| 474 |
|
| 475 |
full_ai = FULL_AI.get()
|
| 476 |
k = CANDIDATES_AI if full_ai else CANDIDATES
|
| 477 |
+
target = change_target() if full_ai else min(90.0, float(VOCAB_TARGET.get() or 0))
|
| 478 |
er = EXTRA_ROUNDS.get()
|
| 479 |
rounds = 1 + ((AI_ROUNDS if er is None else er) if (full_ai or target) else 0)
|
| 480 |
temp0 = min(1.5, float(temperature) + (0.2 if full_ai else 0.0))
|
|
|
|
| 552 |
extra_rounds = meaning_dropped = 0
|
| 553 |
qstat = dict(n=0, meaning=0, fluency=0, second=0, sims=[])
|
| 554 |
ref = [anchor_of(s) or s for s, _, _ in sent_map] # the author's original sentence
|
| 555 |
+
# stricter for technical sentences; a high vocabulary-change setting allows a little more distance (the
|
| 556 |
+
# faithfulness check still rejects changed claims)
|
| 557 |
+
relax = 0.05 if target >= 50 else 0.03 if target >= 35 else 0.0
|
| 558 |
+
thr = [MEANING.min_for(r) - relax for r in ref]
|
| 559 |
+
soft = [MEANING.min_for(r, MEANING.SOFT_SIM) - relax for r in ref]
|
| 560 |
# correct sentences with replaced vocabulary: suitable in-context synonyms, structure unchanged (always available
|
| 561 |
# as a clear option, so a rejected paraphrase is replaced by new wording instead of the original sentence)
|
| 562 |
lex = [[] for _ in sent_map]
|
|
|
|
| 564 |
if progress is not None:
|
| 565 |
progress(lo, desc="Finding suitable replacement vocabulary")
|
| 566 |
try:
|
| 567 |
+
lex = LX.variants(ref, glossary, n=LEX_N, ratios=LX.ratios_for(target, LEX_N + 1) if target else None)
|
| 568 |
except Exception as err:
|
| 569 |
print(f"[humanizer] vocabulary replacement skipped: {err}", flush=True)
|
| 570 |
print(f"[humanizer] vocabulary replacement: {sum(1 for x in lex if x)} of {len(lex)} sentences got "
|
|
|
|
| 749 |
cur = [chosen[si] or sent_map[si][0] for si in want]
|
| 750 |
sc = _detector_scores(cur) or [1.0] * len(cur)
|
| 751 |
now = dict(zip(want, sc))
|
| 752 |
+
short = {si for si in want if target and (chosen[si] is None or change(si, chosen[si]) < target)}
|
| 753 |
+
todo = sorted([si for si in want if chosen[si] is None or now[si] >= RESCUE_AI or si in short],
|
| 754 |
+
key=lambda si: (si not in short or chosen[si] is not None, -now[si]))[:RESCUE_MAX]
|
| 755 |
if todo:
|
| 756 |
vs = None
|
| 757 |
for prov in [G.PROVIDER.get()] + (["openai"] if G.PROVIDER.get() != "openai" and G.openai_available()
|
| 758 |
else []):
|
| 759 |
try:
|
| 760 |
vs = G._in_provider(prov, G.variants, [ref[si] for si in todo], glossary, None, RESCUE_N,
|
| 761 |
+
progress, hi, hi, min(80, target) if target else 0)
|
| 762 |
break
|
| 763 |
except G.Unavailable as err:
|
| 764 |
print(f"[humanizer] rescue rewrites unavailable ({prov}): {err}", flush=True)
|
|
|
|
| 783 |
csc = _detector_scores([v for _, v in cands]) or [0.5] * len(cands)
|
| 784 |
best = {}
|
| 785 |
for (si, v), d in zip(cands, csc):
|
| 786 |
+
# with a vocabulary-change setting, versions that reach it come first, then the most human
|
| 787 |
+
key = ((0 if not target or change(si, v) >= target else 1), d)
|
| 788 |
+
if si not in best or key < best[si][0]:
|
| 789 |
+
best[si] = (key, v)
|
| 790 |
+
for si, ((miss, d), v) in best.items():
|
| 791 |
+
reaches = target and not miss and (chosen[si] is None or change(si, chosen[si]) < target)
|
| 792 |
+
if d < now[si] - 0.05 or (chosen[si] is None and d < now[si]) or (reaches and d <= now[si] + 0.05):
|
| 793 |
chosen[si] = v
|
| 794 |
rescued += 1
|
| 795 |
CL.report()
|
lexsub.py
CHANGED
|
@@ -140,6 +140,9 @@ def _same_form(orig, cand, wn=None):
|
|
| 140 |
o, c = orig.lower(), cand.lower()
|
| 141 |
if o.endswith("ed") and not c.endswith("ed"):
|
| 142 |
return bool(wn) and (wn.morphy(c, "v") or c) != c and not c.endswith(("s", "ing"))
|
|
|
|
|
|
|
|
|
|
| 143 |
for suf in ("ing", "ed", "ly", "s", "est"):
|
| 144 |
if o.endswith(suf) != c.endswith(suf):
|
| 145 |
return False
|
|
@@ -200,9 +203,21 @@ def _protected_words(sentence, glossary):
|
|
| 200 |
|
| 201 |
|
| 202 |
# ------------------------------------------------------------------------------------------------ public API
|
| 203 |
-
def
|
| 204 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
ratio = RATIO if ratio is None else ratio
|
|
|
|
| 206 |
out = [[] for _ in sentences]
|
| 207 |
if not sentences or not ENABLED:
|
| 208 |
return out
|
|
@@ -241,15 +256,16 @@ def variants(sentences, glossary=(), n=2, ratio=None, predict_fn=None):
|
|
| 241 |
for k, items in per.items():
|
| 242 |
s = sentences[k]
|
| 243 |
n_words = len(s.split())
|
| 244 |
-
limit = max(1, round(ratio * n_elig.get(k, 0))) if n_words >= 6 else 1
|
| 245 |
items.sort(key=lambda x: -x[0]) # most predictable words first
|
| 246 |
-
for v in
|
|
|
|
|
|
|
| 247 |
chosen = []
|
| 248 |
for p_orig, (a, b, w), good in items:
|
| 249 |
if len(chosen) >= limit:
|
| 250 |
break
|
| 251 |
-
if any(a <= cb +
|
| 252 |
-
continue
|
| 253 |
c = good[min(v, len(good) - 1)][0]
|
| 254 |
if w[0].isupper():
|
| 255 |
c = c.capitalize()
|
|
|
|
| 140 |
o, c = orig.lower(), cand.lower()
|
| 141 |
if o.endswith("ed") and not c.endswith("ed"):
|
| 142 |
return bool(wn) and (wn.morphy(c, "v") or c) != c and not c.endswith(("s", "ing"))
|
| 143 |
+
if wn and not o.endswith(("ed", "s", "ing")) and set(wn._morphy(o, "v")) - {o}:
|
| 144 |
+
# irregular past form ('found', 'grew', 'made'): the replacement must be a past form too
|
| 145 |
+
return c.endswith("ed") or bool(set(wn._morphy(c, "v")) - {c}) and not c.endswith(("s", "ing"))
|
| 146 |
for suf in ("ing", "ed", "ly", "s", "est"):
|
| 147 |
if o.endswith(suf) != c.endswith(suf):
|
| 148 |
return False
|
|
|
|
| 203 |
|
| 204 |
|
| 205 |
# ------------------------------------------------------------------------------------------------ public API
|
| 206 |
+
def ratios_for(target, n=3):
|
| 207 |
+
"""Replacement shares that aim at a vocabulary-change target (%): content words are about half of an academic
|
| 208 |
+
sentence, so a target of T% needs roughly 2T% of the eligible words replaced; versions straddle that level."""
|
| 209 |
+
if not target:
|
| 210 |
+
return [RATIO] * n
|
| 211 |
+
need = min(1.0, 2.0 * float(target) / 100.0)
|
| 212 |
+
steps = [0.75, 1.0, 1.3, 1.6][:max(1, n)]
|
| 213 |
+
return sorted({round(min(1.0, max(0.2, need * f)), 2) for f in steps})
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def variants(sentences, glossary=(), n=2, ratio=None, predict_fn=None, ratios=None):
|
| 217 |
+
"""Vocabulary-replaced versions of each sentence (list per sentence; [] when nothing to replace): n versions at
|
| 218 |
+
`ratio`, or one version per share in `ratios` (e.g. from ratios_for(target))."""
|
| 219 |
ratio = RATIO if ratio is None else ratio
|
| 220 |
+
plan = list(ratios) if ratios else [ratio] * n
|
| 221 |
out = [[] for _ in sentences]
|
| 222 |
if not sentences or not ENABLED:
|
| 223 |
return out
|
|
|
|
| 256 |
for k, items in per.items():
|
| 257 |
s = sentences[k]
|
| 258 |
n_words = len(s.split())
|
|
|
|
| 259 |
items.sort(key=lambda x: -x[0]) # most predictable words first
|
| 260 |
+
for v, r in enumerate(plan):
|
| 261 |
+
limit = max(1, round(r * n_elig.get(k, 0))) if n_words >= 6 else 1
|
| 262 |
+
gap = 0 if r >= 0.6 else 1 # high targets may replace neighbouring words
|
| 263 |
chosen = []
|
| 264 |
for p_orig, (a, b, w), good in items:
|
| 265 |
if len(chosen) >= limit:
|
| 266 |
break
|
| 267 |
+
if any(a <= cb + gap and ca <= b + gap for ca, cb, _ in chosen):
|
| 268 |
+
continue
|
| 269 |
c = good[min(v, len(good) - 1)][0]
|
| 270 |
if w[0].isupper():
|
| 271 |
c = c.capitalize()
|