Rasheed commited on
Commit
626fb61
·
verified ·
1 Parent(s): b0b5152

Upload 4 files

Browse files
Files changed (4) hide show
  1. clarity.py +7 -1
  2. groqai.py +14 -4
  3. humanizer.py +23 -12
  4. 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
- def _variants_batch(items, glossary, n):
 
 
 
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): st
 
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
- return min(MAX_VOCAB, float(AI_CHANGE_TARGET if v is None else v))
 
 
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(MAX_VOCAB, float(VOCAB_TARGET.get() or 0))
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
- thr = [MEANING.min_for(r) for r in ref] # stricter for technical sentences
552
- soft = [MEANING.min_for(r, MEANING.SOFT_SIM) for r in ref]
 
 
 
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
- todo = sorted([si for si in want if chosen[si] is None or now[si] >= RESCUE_AI],
746
- key=lambda si: -now[si])[:RESCUE_MAX]
 
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
- if si not in best or d < best[si][0]:
779
- best[si] = (d, v)
780
- for si, (d, v) in best.items():
781
- if d < now[si] - 0.05 or chosen[si] is None and d < now[si]:
 
 
 
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 variants(sentences, glossary=(), n=2, ratio=None, predict_fn=None):
204
- """Up to n vocabulary-replaced versions of each sentence (list per sentence; [] when nothing to replace)."""
 
 
 
 
 
 
 
 
 
 
 
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 range(n):
 
 
247
  chosen = []
248
  for p_orig, (a, b, w), good in items:
249
  if len(chosen) >= limit:
250
  break
251
- if any(a <= cb + 1 and ca <= b + 1 for ca, cb, _ in chosen):
252
- continue # never two neighbouring words
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()