Spaces:
Running
Running
Upload 2 files
Browse files- README.md +3 -0
- 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 = [
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 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 |
-
|
| 463 |
-
|
|
|
|
|
|
|
| 464 |
opts, raw0 = [], None
|
| 465 |
-
for
|
| 466 |
-
|
|
|
|
| 467 |
new = TP.merge_chunks(flat[c0:c0 + n], pieces) if n > 1 else pieces[0]
|
| 468 |
raw0 = raw0 or new
|
| 469 |
-
|
| 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 |
-
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
|
| 484 |
-
|
| 485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 502 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 565 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|