Rasheed commited on
Commit
2bb56a7
·
verified ·
1 Parent(s): 7f9b4c2

Upload 2 files

Browse files
Files changed (2) hide show
  1. README.md +3 -0
  2. humanizer.py +100 -55
README.md CHANGED
@@ -182,6 +182,9 @@ in your privacy policy.
182
  plain-wording rules add more options, the detector ranks them, and Groq checks the 2 most human versions
183
  (`HUM_GROQ_SHORTLIST`). The most human version Groq accepts is kept; if both are rejected, the next
184
  version or the original sentence is used. Parrot still does all the rewriting; Groq only checks.
 
 
 
185
  - Speed: sentences are batched shortest-first and generation stops just past the input length; Groq batches run
186
  3 at a time (`GROQ_PARALLEL`).
187
 
 
182
  plain-wording rules add more options, the detector ranks them, and Groq checks the 2 most human versions
183
  (`HUM_GROQ_SHORTLIST`). The most human version Groq accepts is kept; if both are rejected, the next
184
  version or the original sentence is used. Parrot still does all the rewriting; Groq only checks.
185
+ It also aims for 70% new wording per sentence (`HUM_AI_CHANGE_TARGET`, same measure as the "Vocabulary
186
+ changed" gauge): sentences below it get up to 2 extra Parrot rounds with more variation (`HUM_AI_ROUNDS`),
187
+ versions that reach it are preferred, and Groq's check may not pull the wording back toward the original.
188
  - Speed: sentences are batched shortest-first and generation stops just past the input length; Groq batches run
189
  3 at a time (`GROQ_PARALLEL`).
190
 
humanizer.py CHANGED
@@ -381,6 +381,10 @@ CANDIDATES = max(1, int(os.getenv("HUM_CANDIDATES", "3")))
381
  # most human versions of each sentence (default 2) so a rejected best version falls back to a checked one.
382
  CANDIDATES_AI = max(1, int(os.getenv("HUM_CANDIDATES_AI", "5")))
383
  GROQ_SHORTLIST = max(1, int(os.getenv("HUM_GROQ_SHORTLIST", "2")))
 
 
 
 
384
  FULL_AI = contextvars.ContextVar("full_ai", default=False)
385
  # Kept for compatibility with smart.py / app.py versions that set it per pass. The 100% AI mode is now
386
  # "5 Parrot versions + Groq check" (Parrot does all rewriting), so this switch has no effect.
@@ -421,53 +425,55 @@ def _paraphrase_local(texts, temperature, progress=None, lo=0.15, hi=0.85, mode=
421
 
422
  full_ai = FULL_AI.get()
423
  k = CANDIDATES_AI if full_ai else CANDIDATES
 
 
 
424
  p_hi = lo + (hi - lo) * (0.7 if G.available() else 0.85) # room on the bar for scoring / Groq
 
425
  use_gpu = ON_GPU and not str(mode).lower().startswith("cpu") # CPU-only hardware: straight to CPU
426
  fell_back = False
427
- cand = [None] * len(flat) # cand[i] = list of k paraphrases of flat[i]
428
- order = sorted(range(len(flat)), key=lambda x: len(flat[x]))
429
- total = max(1, len(flat))
430
- step = GPU_CHUNK if use_gpu else CPU_CHUNK
431
- i = 0
432
- while i < len(order):
433
- idx = order[i:i + step]
434
- chunk = [flat[x] for x in idx]
435
- engine = "GPU" if use_gpu else "CPU"
436
- if progress is not None:
437
- progress(lo + (p_hi - lo) * i / total,
438
- desc=f"Paraphrasing {i + 1}–{i + len(chunk)} of {total} ({engine}, {k} versions each)")
439
- if use_gpu:
440
- try:
441
- outs = paraphrase_batch_gpu(chunk, temperature, k)
442
- except Exception as err:
443
- if not _is_gpu_unavailable(err):
444
- raise
445
- use_gpu, fell_back, step = False, True, CPU_CHUNK
446
- continue # retry the same chunk on CPU
447
- else:
448
- outs = paraphrase_batch_cpu(chunk, temperature, k)
449
- for n_, x in enumerate(idx):
450
- cand[x] = outs[n_ * k:(n_ + 1) * k] or [flat[x]]
451
- i += len(chunk)
452
-
453
- if fell_back:
454
- note = "⚠️ GPU quota was unavailable, so the remaining text was processed on CPU (slower, same quality)."
455
- elif use_gpu:
456
- note = "⚡ Processed on GPU."
457
- else:
458
- note = "🖥️ Processed on CPU."
459
 
460
- # ---- 2. valid candidates per sentence (terms restored, guard passed)
461
  glossary = TP.build_glossary(texts) | TP.GLOSSARY.get()
462
- options, first_raw = [], []
463
- for s, c0, n in sent_map:
 
 
464
  opts, raw0 = [], None
465
- for j in range(k):
466
- pieces = [cand[c][j if j < len(cand[c]) else -1] for c in range(c0, c0 + n)]
 
467
  new = TP.merge_chunks(flat[c0:c0 + n], pieces) if n > 1 else pieces[0]
468
  raw0 = raw0 or new
469
- fixed = TP.restore_terms(s, new, glossary)
470
- ok = TP.guard(s, fixed, glossary)
471
  if ok and ok.strip() != s.strip() and ok not in opts:
472
  opts.append(ok)
473
  # "100% AI-written" mode only: plain-wording versions (chatbot vocabulary / set phrases removed) of each
@@ -478,11 +484,40 @@ def _paraphrase_local(texts, temperature, progress=None, lo=0.15, hi=0.85, mode=
478
  ok = TP.guard(s, TP.restore_terms(s, v, glossary), glossary)
479
  if ok and ok.strip() != s.strip() and ok not in opts:
480
  opts.append(ok)
481
- options.append(opts)
482
- first_raw.append(raw0 or s)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
 
484
- # ---- 3. most human candidate (AI detector)
485
- chosen = [o[0] if o else None for o in options]
 
 
 
 
 
 
486
  score_of = {}
487
  multi = [(si, o) for si, opts in enumerate(options) if len(opts) > 1 for o in opts]
488
  if SELECT and multi:
@@ -490,17 +525,19 @@ def _paraphrase_local(texts, temperature, progress=None, lo=0.15, hi=0.85, mode=
490
  progress(p_hi, desc="Choosing the most human-sounding version of each sentence")
491
  sc = _detector_scores([o for _, o in multi])
492
  if sc:
493
- best = {}
494
- for (si, o), v in zip(multi, sc):
495
- score_of[(si, o)] = v
496
- if si not in best or v < best[si][0]:
497
- best[si] = (v, o)
498
- for si, (v, o) in best.items():
499
- chosen[si] = o
500
  note += f" 🎯 Best of {k} Parrot versions per sentence chosen by the AI detector."
501
- # candidates of each sentence, most human first (unscored -> original order)
502
- ranked = [sorted(opts, key=lambda o, si=si: score_of.get((si, o), 1.0)) if score_of else list(opts)
 
 
 
 
 
 
 
503
  for si, opts in enumerate(options)]
 
504
 
505
  # ---- 4. Groq: academic-wording check of the chosen sentences + rescue of sentences with no valid version.
506
  # 100% AI mode: Groq checks the GROQ_SHORTLIST most human versions of each sentence, and the most human
@@ -561,8 +598,11 @@ def _paraphrase_local(texts, temperature, progress=None, lo=0.15, hi=0.85, mode=
561
  sc = _detector_scores([ok for _, ok, _ in fixes] + [c for _, _, c in fixes]) if fixes else []
562
  m = len(fixes)
563
  for n_, (si, ok, cand_) in enumerate(fixes):
564
- if not sc or sc[n_] <= sc[m + n_] + 0.02: # never trade AI score for polish
565
- chosen[si] = ok
 
 
 
566
  note += (f" ✅ Groq checked the top {shortlist} versions of each sentence." if shortlist > 1
567
  else " ✅ Academic wording checked by Groq.")
568
  except G.Unavailable as err:
@@ -571,7 +611,12 @@ def _paraphrase_local(texts, temperature, progress=None, lo=0.15, hi=0.85, mode=
571
 
572
  sentences = [chosen[si] or s for si, (s, _, _) in enumerate(sent_map)]
573
  changed = sum(1 for si, (s, _, _) in enumerate(sent_map) if sentences[si].strip() != s.strip())
574
- note += (f" 🤖 100% AI mode ({k} Parrot versions per sentence)." if full_ai else "") + f" ✍️ {changed} of {len(sent_map)} sentences rewritten."
 
 
 
 
 
575
  if changed < len(sent_map):
576
  print(f"[humanizer] kept {len(sent_map) - changed} sentences unchanged "
577
  f"(no safe version: {sum(1 for o in options if not o)})", flush=True)
 
381
  # most human versions of each sentence (default 2) so a rejected best version falls back to a checked one.
382
  CANDIDATES_AI = max(1, int(os.getenv("HUM_CANDIDATES_AI", "5")))
383
  GROQ_SHORTLIST = max(1, int(os.getenv("HUM_GROQ_SHORTLIST", "2")))
384
+ # 100% AI mode: target share of new wording per sentence (same measure as the "Vocabulary changed" gauge)
385
+ # and the number of extra Parrot rounds for sentences below it.
386
+ AI_CHANGE_TARGET = float(os.getenv("HUM_AI_CHANGE_TARGET", "70"))
387
+ AI_ROUNDS = max(0, int(os.getenv("HUM_AI_ROUNDS", "2")))
388
  FULL_AI = contextvars.ContextVar("full_ai", default=False)
389
  # Kept for compatibility with smart.py / app.py versions that set it per pass. The 100% AI mode is now
390
  # "5 Parrot versions + Groq check" (Parrot does all rewriting), so this switch has no effect.
 
425
 
426
  full_ai = FULL_AI.get()
427
  k = CANDIDATES_AI if full_ai else CANDIDATES
428
+ target = AI_CHANGE_TARGET if full_ai else 0.0
429
+ rounds = 1 + (AI_ROUNDS if full_ai else 0)
430
+ temp0 = min(1.5, float(temperature) + (0.2 if full_ai else 0.0))
431
  p_hi = lo + (hi - lo) * (0.7 if G.available() else 0.85) # room on the bar for scoring / Groq
432
+ p_gen = lo + (p_hi - lo) * (0.75 if full_ai else 1.0) # 100% AI: room for the extra rounds
433
  use_gpu = ON_GPU and not str(mode).lower().startswith("cpu") # CPU-only hardware: straight to CPU
434
  fell_back = False
435
+ cand = [[] for _ in flat] # cand[i] = all paraphrases of flat[i] (k per round)
436
+
437
+ def generate(indices, temp, a_, b_, label):
438
+ nonlocal use_gpu, fell_back
439
+ order = sorted(indices, key=lambda x: len(flat[x]))
440
+ total = max(1, len(order))
441
+ step = GPU_CHUNK if use_gpu else CPU_CHUNK
442
+ i = 0
443
+ while i < len(order):
444
+ idx = order[i:i + step]
445
+ chunk = [flat[x] for x in idx]
446
+ engine = "GPU" if use_gpu else "CPU"
447
+ if progress is not None:
448
+ progress(a_ + (b_ - a_) * i / total,
449
+ desc=f"{label} {i + 1}–{i + len(chunk)} of {total} ({engine}, {k} versions each)")
450
+ if use_gpu:
451
+ try:
452
+ outs = paraphrase_batch_gpu(chunk, temp, k)
453
+ except Exception as err:
454
+ if not _is_gpu_unavailable(err):
455
+ raise
456
+ use_gpu, fell_back, step = False, True, CPU_CHUNK
457
+ continue # retry the same chunk on CPU
458
+ else:
459
+ outs = paraphrase_batch_cpu(chunk, temp, k)
460
+ for n_, x in enumerate(idx):
461
+ got = outs[n_ * k:(n_ + 1) * k] or [flat[x]]
462
+ cand[x].extend(got + [got[-1]] * (k - len(got))) # keep k per round so chunks line up
463
+ i += len(chunk)
 
 
 
464
 
 
465
  glossary = TP.build_glossary(texts) | TP.GLOSSARY.get()
466
+
467
+ def build(si):
468
+ """Valid candidates of sentence si (terms restored, guard passed) + first raw paraphrase."""
469
+ s, c0, n = sent_map[si]
470
  opts, raw0 = [], None
471
+ m = min(len(cand[c]) for c in range(c0, c0 + n)) if n else 0
472
+ for j in range(m):
473
+ pieces = [cand[c][j] for c in range(c0, c0 + n)]
474
  new = TP.merge_chunks(flat[c0:c0 + n], pieces) if n > 1 else pieces[0]
475
  raw0 = raw0 or new
476
+ ok = TP.guard(s, TP.restore_terms(s, new, glossary), glossary)
 
477
  if ok and ok.strip() != s.strip() and ok not in opts:
478
  opts.append(ok)
479
  # "100% AI-written" mode only: plain-wording versions (chatbot vocabulary / set phrases removed) of each
 
484
  ok = TP.guard(s, TP.restore_terms(s, v, glossary), glossary)
485
  if ok and ok.strip() != s.strip() and ok not in opts:
486
  opts.append(ok)
487
+ return opts, raw0 or s
488
+
489
+ def change(si, o):
490
+ return words_changed(sent_map[si][0], o)
491
+
492
+ # ---- 1–2. Parrot versions; in 100% AI mode, sentences below the new-wording target (or with no safe
493
+ # version) go back to Parrot for extra rounds with more variation
494
+ todo = list(range(len(sent_map)))
495
+ options, first_raw = [[] for _ in sent_map], [s for s, _, _ in sent_map]
496
+ extra_rounds = 0
497
+ for r in range(rounds):
498
+ if not todo:
499
+ break
500
+ a_ = lo if r == 0 else p_gen + (p_hi - p_gen) * (r - 1) / max(1, rounds - 1)
501
+ b_ = p_gen if r == 0 else p_gen + (p_hi - p_gen) * r / max(1, rounds - 1)
502
+ chunks = [c for si in todo for c in range(sent_map[si][1], sent_map[si][1] + sent_map[si][2])]
503
+ generate(chunks, min(1.5, temp0 + 0.25 * r), a_, b_,
504
+ "Paraphrasing" if r == 0 else f"Extra rewrite round {r} for")
505
+ extra_rounds += r > 0
506
+ for si in todo:
507
+ options[si], first_raw[si] = build(si)
508
+ if not target:
509
+ break
510
+ todo = [si for si in todo
511
+ if not options[si] or max(change(si, o) for o in options[si]) < target]
512
 
513
+ if fell_back:
514
+ note = "⚠️ GPU quota was unavailable, so the remaining text was processed on CPU (slower, same quality)."
515
+ elif use_gpu:
516
+ note = "⚡ Processed on GPU."
517
+ else:
518
+ note = "🖥️ Processed on CPU."
519
+
520
+ # ---- 3. most human candidate (AI detector); 100% AI mode: versions meeting the new-wording target first
521
  score_of = {}
522
  multi = [(si, o) for si, opts in enumerate(options) if len(opts) > 1 for o in opts]
523
  if SELECT and multi:
 
525
  progress(p_hi, desc="Choosing the most human-sounding version of each sentence")
526
  sc = _detector_scores([o for _, o in multi])
527
  if sc:
528
+ score_of = {key: v for key, v in zip(multi, sc)}
 
 
 
 
 
 
529
  note += f" 🎯 Best of {k} Parrot versions per sentence chosen by the AI detector."
530
+
531
+ def rank_key(si, o):
532
+ det = score_of.get((si, o), 1.0 if score_of else 0.0)
533
+ if target:
534
+ ch = change(si, o)
535
+ return (0, det) if ch >= target else (1, -ch, det)
536
+ return (det,)
537
+
538
+ ranked = [sorted(opts, key=lambda o, si=si: rank_key(si, o)) if (score_of or target) else list(opts)
539
  for si, opts in enumerate(options)]
540
+ chosen = [r_[0] if r_ else None for r_ in ranked]
541
 
542
  # ---- 4. Groq: academic-wording check of the chosen sentences + rescue of sentences with no valid version.
543
  # 100% AI mode: Groq checks the GROQ_SHORTLIST most human versions of each sentence, and the most human
 
598
  sc = _detector_scores([ok for _, ok, _ in fixes] + [c for _, _, c in fixes]) if fixes else []
599
  m = len(fixes)
600
  for n_, (si, ok, cand_) in enumerate(fixes):
601
+ if sc and sc[n_] > sc[m + n_] + 0.02: # never trade AI score for polish
602
+ continue
603
+ if target and change(si, ok) < min(target, change(si, cand_) - 10):
604
+ continue # 100% AI: Groq must not undo the rewrite
605
+ chosen[si] = ok
606
  note += (f" ✅ Groq checked the top {shortlist} versions of each sentence." if shortlist > 1
607
  else " ✅ Academic wording checked by Groq.")
608
  except G.Unavailable as err:
 
611
 
612
  sentences = [chosen[si] or s for si, (s, _, _) in enumerate(sent_map)]
613
  changed = sum(1 for si, (s, _, _) in enumerate(sent_map) if sentences[si].strip() != s.strip())
614
+ if full_ai:
615
+ hit = sum(1 for si, (s0, _, _) in enumerate(sent_map)
616
+ if sentences[si].strip() != s0.strip() and change(si, sentences[si]) >= target)
617
+ note += (f" 🤖 100% AI mode: {k} Parrot versions per sentence"
618
+ + (f", {extra_rounds} extra round(s)" if extra_rounds else "")
619
+ + f"; {hit} of {len(sent_map)} sentences reach {target:.0f}% new wording.") + f" ✍️ {changed} of {len(sent_map)} sentences rewritten."
620
  if changed < len(sent_map):
621
  print(f"[humanizer] kept {len(sent_map) - changed} sentences unchanged "
622
  f"(no safe version: {sum(1 for o in options if not o)})", flush=True)