Spaces:
Running
Running
unknown Claude Opus 5.5 commited on
Commit ·
45c0ee6
1
Parent(s): 3089e27
0% first: rounds continue until the document is at 0% (10-minute safety cap); documents not at 0% are never charged
Browse files- fast.py: no one-minute stop any more: rounds go on until 0% on our detector, until 3 rounds bring no improvement,
or the FAST_BUDGET cap (600 s); later rounds give stubborn sentences more versions and the strongest rewrite. The
progress line shows the current score every round.
- app.py: a document that did not reach 0% is delivered with the remaining sentences highlighted, the customer is
told, and it is not charged (no pay-per-use payment, no plan credit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- app.py +14 -2
- fast.py +26 -14
- tests/test_app_rephrase_anyway.py +24 -0
app.py
CHANGED
|
@@ -480,6 +480,13 @@ def _smart_result(res, hi, mod, text_mode, rep=None, name=""):
|
|
| 480 |
if locked:
|
| 481 |
msg += (f"\n\n🔒 {locked} flagged sentence(s) could not be edited (citation fields, links, equations or "
|
| 482 |
"sub/superscripts inside the sentence) and keep their wording.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
labels, g_before, g_after = None, b["pct"], a["pct"]
|
| 484 |
gline = _grammar_line(res.get("note"))
|
| 485 |
rs = res.get("report")
|
|
@@ -522,12 +529,16 @@ def _smart_result(res, hi, mod, text_mode, rep=None, name=""):
|
|
| 522 |
rep_score=(rs or {}).get("score"), rep_est=(rs or {}).get("est"))
|
| 523 |
status = H.status_done(msg) + (already_human_html(text_mode) if untouched and not FORCED.get() else "")
|
| 524 |
out = [status, preview, panel, res["path"], H.download_button(res["path"])]
|
| 525 |
-
charge = None if text_mode or untouched or stuck else _payg_lock(
|
| 526 |
res["path"], "Smart Humanize", name, b["all_words"], res.get("seconds", 0), rep)
|
| 527 |
if stuck and PAYG_FOR.get() and not text_mode:
|
| 528 |
out[0] = H.status_done(msg + "\n\n🎁 **No charge for this run**, because the score was not lowered.")
|
| 529 |
if rep is not None and hasattr(rep, "meta"): # given away, not bought: it earns no free re-runs
|
| 530 |
rep.meta.update(no_charge=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
if charge: # pay per use: scores now, the document after payment
|
| 532 |
out = [H.status_done(msg) + pay_popup_html(charge, "smart"), LOCKED_PREVIEW, panel, None, ""]
|
| 533 |
if text_mode:
|
|
@@ -2648,7 +2659,8 @@ demo.queue(default_concurrency_limit=int(os.getenv("CONCURRENCY", "4")),
|
|
| 2648 |
|
| 2649 |
if os.getenv("FAST_SELFTEST") == "1": # timing of the fast engine on the Space's own hardware
|
| 2650 |
import threading
|
| 2651 |
-
|
|
|
|
| 2652 |
|
| 2653 |
if __name__ == "__main__":
|
| 2654 |
demo.launch(show_api=False, ssr_mode=False, allowed_paths=[str(OUT_DIR)], max_threads=80,
|
|
|
|
| 480 |
if locked:
|
| 481 |
msg += (f"\n\n🔒 {locked} flagged sentence(s) could not be edited (citation fields, links, equations or "
|
| 482 |
"sub/superscripts inside the sentence) and keep their wording.")
|
| 483 |
+
# 0% is the promise: a document that did not reach it is delivered with the sentences still flagged highlighted,
|
| 484 |
+
# and it is not charged (no pay-per-use payment, no credit used)
|
| 485 |
+
short = bool(res.get("zero_goal")) and a["pct"] > 0 and not res.get("report") and not stuck
|
| 486 |
+
if short:
|
| 487 |
+
msg = (f"⚠️ **AI score {b['pct']}% → {a['pct']}%.** {res.get('left') or 'A few'} sentence(s) still read as "
|
| 488 |
+
"AI-written and could not be rewritten further without changing what they say; they are "
|
| 489 |
+
"highlighted in the AI view below so you can adjust them yourself.")
|
| 490 |
labels, g_before, g_after = None, b["pct"], a["pct"]
|
| 491 |
gline = _grammar_line(res.get("note"))
|
| 492 |
rs = res.get("report")
|
|
|
|
| 529 |
rep_score=(rs or {}).get("score"), rep_est=(rs or {}).get("est"))
|
| 530 |
status = H.status_done(msg) + (already_human_html(text_mode) if untouched and not FORCED.get() else "")
|
| 531 |
out = [status, preview, panel, res["path"], H.download_button(res["path"])]
|
| 532 |
+
charge = None if text_mode or untouched or stuck or short else _payg_lock(
|
| 533 |
res["path"], "Smart Humanize", name, b["all_words"], res.get("seconds", 0), rep)
|
| 534 |
if stuck and PAYG_FOR.get() and not text_mode:
|
| 535 |
out[0] = H.status_done(msg + "\n\n🎁 **No charge for this run**, because the score was not lowered.")
|
| 536 |
if rep is not None and hasattr(rep, "meta"): # given away, not bought: it earns no free re-runs
|
| 537 |
rep.meta.update(no_charge=True)
|
| 538 |
+
if short and not text_mode:
|
| 539 |
+
out[0] = H.status_done(msg + "\n\n🎁 **No charge for this document**, because it did not reach 0%.")
|
| 540 |
+
if rep is not None and hasattr(rep, "meta"): # no pay-per-use payment and no credit from the plan
|
| 541 |
+
rep.meta.update(no_charge=True)
|
| 542 |
if charge: # pay per use: scores now, the document after payment
|
| 543 |
out = [H.status_done(msg) + pay_popup_html(charge, "smart"), LOCKED_PREVIEW, panel, None, ""]
|
| 544 |
if text_mode:
|
|
|
|
| 2659 |
|
| 2660 |
if os.getenv("FAST_SELFTEST") == "1": # timing of the fast engine on the Space's own hardware
|
| 2661 |
import threading
|
| 2662 |
+
_sizes = tuple(int(x) for x in os.getenv("FAST_SELFTEST_SIZES", "1,3,8").split(",") if x.strip())
|
| 2663 |
+
threading.Thread(target=F.selftest, args=(_sizes,), daemon=True).start()
|
| 2664 |
|
| 2665 |
if __name__ == "__main__":
|
| 2666 |
demo.launch(show_api=False, ssr_mode=False, allowed_paths=[str(OUT_DIR)], max_threads=80,
|
fast.py
CHANGED
|
@@ -38,21 +38,26 @@ import pubrules as PR
|
|
| 38 |
import smart as S
|
| 39 |
import textparse as TP
|
| 40 |
|
| 41 |
-
#
|
| 42 |
-
#
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
| 44 |
PARALLEL = int(os.getenv("FAST_PARALLEL", "16"))
|
| 45 |
# sentences per request: the instructions (~1,500 tokens) go with every request, so larger batches cost far fewer
|
| 46 |
# tokens per sentence (they were most of the tokens sent)
|
| 47 |
BATCH = int(os.getenv("FAST_BATCH", "24"))
|
| 48 |
ALWAYS_ROUNDS = int(os.getenv("FAST_ALWAYS_ROUNDS", "1")) # rounds in which every flagged sentence is replaced
|
| 49 |
-
ROUNDS = int(os.getenv("FAST_ROUNDS", "
|
| 50 |
VERSIONS = int(os.getenv("FAST_VERSIONS", "2")) # per model (4 models: 8 versions per sentence)
|
| 51 |
# round -> (versions per sentence, share of new wording asked for (%), sentences above this score are rewritten)
|
| 52 |
# FAST_CHANGES: the new-wording share asked for in each round (measured on 4 AI paragraphs with the 3-model detector:
|
| 53 |
# "0,40,60,70" needed 4 rounds for 0%; the first, gentle round only took the score from 100% to 79%)
|
| 54 |
_CHG = [int(x) for x in os.getenv("FAST_CHANGES", "50,60,70,75").split(",") if x.strip()]
|
| 55 |
PLAN = [(VERSIONS + min(k, 2), c, thr) for k, (c, thr) in enumerate(zip(_CHG, (0.50, 0.40, 0.30, 0.25)))]
|
|
|
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
def _stock(s):
|
|
@@ -390,8 +395,9 @@ def humanize_docx(path, hi=0.80, mod=0.50, rep=None, budget=None):
|
|
| 390 |
best = (stats["pct"], S._snapshot(doc), blocks)
|
| 391 |
anchor = {} # current sentence -> the author's sentence
|
| 392 |
log, note = [], ""
|
| 393 |
-
end = t0 + budget # the job's
|
| 394 |
last_lm = last_after = None
|
|
|
|
| 395 |
for rnd, (n, chg, thr) in enumerate(PLAN[:ROUNDS], 1):
|
| 396 |
if CPU_VERSIONS and D.on_cpu():
|
| 397 |
n = min(n, CPU_VERSIONS)
|
|
@@ -402,15 +408,14 @@ def humanize_docx(path, hi=0.80, mod=0.50, rep=None, budget=None):
|
|
| 402 |
break
|
| 403 |
# time for choosing + re-scoring after the language model (measured on the T4: about 0.03 s per sentence)
|
| 404 |
after_lm = 4 + 0.035 * len(jobs) if last_after is None else last_after * 1.15
|
| 405 |
-
left = end - time.time() - after_lm
|
| 406 |
-
if rnd > 1 and left < 8.0: #
|
| 407 |
-
|
| 408 |
-
log.append(f"Round {rnd}: stopped, time budget used")
|
| 409 |
break
|
| 410 |
texts = [j[2].strip() for j in jobs]
|
| 411 |
anchors = [anchor.get(t, t) for t in texts]
|
| 412 |
-
frac = 0.12 + 0.
|
| 413 |
-
rep(frac, f"Round {rnd}: rewriting {len(jobs)} sentence(s) at once")
|
| 414 |
tg = time.time()
|
| 415 |
try:
|
| 416 |
outs = _versions(texts, anchors, glossary, n, chg, lower, deadline=time.time() + max(8.0, left))
|
|
@@ -419,7 +424,7 @@ def humanize_docx(path, hi=0.80, mod=0.50, rep=None, budget=None):
|
|
| 419 |
print(f"[fast] round {rnd}: rewriting unavailable: {err}", flush=True)
|
| 420 |
break
|
| 421 |
tg = time.time() - tg
|
| 422 |
-
rep(frac + 0.
|
| 423 |
tc = time.time()
|
| 424 |
# round 1: every flagged sentence takes its best valid version, even one that scores no lower on its own: a
|
| 425 |
# window is only read as human when (nearly) all its sentences changed; on a 5,500-word document the "must
|
|
@@ -445,7 +450,13 @@ def humanize_docx(path, hi=0.80, mod=0.50, rep=None, budget=None):
|
|
| 445 |
+ ", ".join(f"{k} {v:.1f}s" for k, v in TIMES.items()) + f"), scoring {ts:.1f}s", flush=True)
|
| 446 |
if stats["pct"] < best[0]:
|
| 447 |
best = (stats["pct"], S._snapshot(doc), blocks)
|
| 448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
from docx import Document as _Doc
|
| 450 |
doc = _Doc(io.BytesIO(best[1]))
|
| 451 |
blocks = S._score_doc(doc)
|
|
@@ -462,8 +473,9 @@ def humanize_docx(path, hi=0.80, mod=0.50, rep=None, budget=None):
|
|
| 462 |
print(f"[fast] {first['pct']}% -> {after['pct']}% in {secs:.1f}s ({first['analysed']} words) | "
|
| 463 |
f"{G.usage_line()}", flush=True)
|
| 464 |
note = "⚡ Fast engine: every flagged sentence rewritten at once by the language model." + note
|
|
|
|
| 465 |
return dict(before=first, after=after, log=log, note=note, blocks=blocks, path=out, report=None,
|
| 466 |
-
orig=orig_text, new=new_text, seconds=secs)
|
| 467 |
|
| 468 |
|
| 469 |
def humanize_text(text, hi=0.80, mod=0.50, rep=None, budget=None):
|
|
|
|
| 38 |
import smart as S
|
| 39 |
import textparse as TP
|
| 40 |
|
| 41 |
+
# 0% first: rounds continue until the document is at 0% on our detector (each round rewriting more strongly), until
|
| 42 |
+
# STALL rounds in a row bring no improvement, or until FAST_BUDGET seconds (a safety cap; about a minute for 2,000
|
| 43 |
+
# words, a few minutes for a thesis on a 250k-tokens-a-minute Groq key). Requests still running at the cap are dropped.
|
| 44 |
+
BUDGET = float(os.getenv("FAST_BUDGET", "600"))
|
| 45 |
+
ROUND_LM_MAX = float(os.getenv("FAST_ROUND_LM_MAX", "90")) # seconds one round may wait for the language models
|
| 46 |
+
STALL = int(os.getenv("FAST_STALL", "3"))
|
| 47 |
PARALLEL = int(os.getenv("FAST_PARALLEL", "16"))
|
| 48 |
# sentences per request: the instructions (~1,500 tokens) go with every request, so larger batches cost far fewer
|
| 49 |
# tokens per sentence (they were most of the tokens sent)
|
| 50 |
BATCH = int(os.getenv("FAST_BATCH", "24"))
|
| 51 |
ALWAYS_ROUNDS = int(os.getenv("FAST_ALWAYS_ROUNDS", "1")) # rounds in which every flagged sentence is replaced
|
| 52 |
+
ROUNDS = int(os.getenv("FAST_ROUNDS", "10"))
|
| 53 |
VERSIONS = int(os.getenv("FAST_VERSIONS", "2")) # per model (4 models: 8 versions per sentence)
|
| 54 |
# round -> (versions per sentence, share of new wording asked for (%), sentences above this score are rewritten)
|
| 55 |
# FAST_CHANGES: the new-wording share asked for in each round (measured on 4 AI paragraphs with the 3-model detector:
|
| 56 |
# "0,40,60,70" needed 4 rounds for 0%; the first, gentle round only took the score from 100% to 79%)
|
| 57 |
_CHG = [int(x) for x in os.getenv("FAST_CHANGES", "50,60,70,75").split(",") if x.strip()]
|
| 58 |
PLAN = [(VERSIONS + min(k, 2), c, thr) for k, (c, thr) in enumerate(zip(_CHG, (0.50, 0.40, 0.30, 0.25)))]
|
| 59 |
+
# rounds after the plan: the sentences left are the stubborn ones, so more versions and the strongest rewrite
|
| 60 |
+
PLAN += [(VERSIONS + 3, max(_CHG[-1], 80), 0.25)] * max(0, ROUNDS - len(PLAN))
|
| 61 |
|
| 62 |
|
| 63 |
def _stock(s):
|
|
|
|
| 395 |
best = (stats["pct"], S._snapshot(doc), blocks)
|
| 396 |
anchor = {} # current sentence -> the author's sentence
|
| 397 |
log, note = [], ""
|
| 398 |
+
end = t0 + budget # the job's safety cap
|
| 399 |
last_lm = last_after = None
|
| 400 |
+
stalled = 0
|
| 401 |
for rnd, (n, chg, thr) in enumerate(PLAN[:ROUNDS], 1):
|
| 402 |
if CPU_VERSIONS and D.on_cpu():
|
| 403 |
n = min(n, CPU_VERSIONS)
|
|
|
|
| 408 |
break
|
| 409 |
# time for choosing + re-scoring after the language model (measured on the T4: about 0.03 s per sentence)
|
| 410 |
after_lm = 4 + 0.035 * len(jobs) if last_after is None else last_after * 1.15
|
| 411 |
+
left = min(ROUND_LM_MAX, end - time.time() - after_lm)
|
| 412 |
+
if rnd > 1 and left < 8.0: # the safety cap is reached
|
| 413 |
+
log.append(f"Round {rnd}: stopped, time limit reached")
|
|
|
|
| 414 |
break
|
| 415 |
texts = [j[2].strip() for j in jobs]
|
| 416 |
anchors = [anchor.get(t, t) for t in texts]
|
| 417 |
+
frac = 0.12 + 0.83 * (1 - 0.6 ** (rnd - 1)) # moves on every round without promising an end
|
| 418 |
+
rep(frac, f"Round {rnd}: AI score {stats['pct']}%, rewriting {len(jobs)} sentence(s) at once")
|
| 419 |
tg = time.time()
|
| 420 |
try:
|
| 421 |
outs = _versions(texts, anchors, glossary, n, chg, lower, deadline=time.time() + max(8.0, left))
|
|
|
|
| 424 |
print(f"[fast] round {rnd}: rewriting unavailable: {err}", flush=True)
|
| 425 |
break
|
| 426 |
tg = time.time() - tg
|
| 427 |
+
rep(frac + 0.83 * 0.6 ** (rnd - 1) * 0.2, f"Round {rnd}: choosing the most natural version of each sentence")
|
| 428 |
tc = time.time()
|
| 429 |
# round 1: every flagged sentence takes its best valid version, even one that scores no lower on its own: a
|
| 430 |
# window is only read as human when (nearly) all its sentences changed; on a 5,500-word document the "must
|
|
|
|
| 450 |
+ ", ".join(f"{k} {v:.1f}s" for k, v in TIMES.items()) + f"), scoring {ts:.1f}s", flush=True)
|
| 451 |
if stats["pct"] < best[0]:
|
| 452 |
best = (stats["pct"], S._snapshot(doc), blocks)
|
| 453 |
+
stalled = 0
|
| 454 |
+
else:
|
| 455 |
+
stalled += 1
|
| 456 |
+
if stalled >= STALL: # STALL rounds in a row without a lower score
|
| 457 |
+
log.append(f"Round {rnd}: stopped, {STALL} rounds without further improvement")
|
| 458 |
+
break
|
| 459 |
+
if stats["pct"] > best[0]: # this round made it worse: the next one starts from the best
|
| 460 |
from docx import Document as _Doc
|
| 461 |
doc = _Doc(io.BytesIO(best[1]))
|
| 462 |
blocks = S._score_doc(doc)
|
|
|
|
| 473 |
print(f"[fast] {first['pct']}% -> {after['pct']}% in {secs:.1f}s ({first['analysed']} words) | "
|
| 474 |
f"{G.usage_line()}", flush=True)
|
| 475 |
note = "⚡ Fast engine: every flagged sentence rewritten at once by the language model." + note
|
| 476 |
+
left_ = [s_ for b_ in blocks if b_.kind == "para" for s_, p_ in zip(b_.sents, b_.scores) if p_ >= mod]
|
| 477 |
return dict(before=first, after=after, log=log, note=note, blocks=blocks, path=out, report=None,
|
| 478 |
+
orig=orig_text, new=new_text, seconds=secs, zero_goal=True, left=len(left_))
|
| 479 |
|
| 480 |
|
| 481 |
def humanize_text(text, hi=0.80, mod=0.50, rep=None, budget=None):
|
tests/test_app_rephrase_anyway.py
CHANGED
|
@@ -134,3 +134,27 @@ def test_a_free_stuck_pay_per_use_run_is_marked_so_it_earns_no_free_reruns(monke
|
|
| 134 |
app.PAYG_FOR.reset(tok)
|
| 135 |
src = inspect.getsource(app._stream)
|
| 136 |
assert 'if not meta.get("no_charge")' in src and src.index('meta.get("no_charge")') < src.index("P.consume_doc")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
app.PAYG_FOR.reset(tok)
|
| 135 |
src = inspect.getsource(app._stream)
|
| 136 |
assert 'if not meta.get("no_charge")' in src and src.index('meta.get("no_charge")') < src.index("P.consume_doc")
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def test_a_document_not_at_zero_percent_is_never_charged(monkeypatch):
|
| 140 |
+
"""0% is the promise: a document the engine could not bring to 0% is delivered (remaining sentences highlighted)
|
| 141 |
+
with no pay-per-use payment and no plan credit; a document at 0% is charged as usual."""
|
| 142 |
+
class Rep:
|
| 143 |
+
meta = {}
|
| 144 |
+
|
| 145 |
+
def __call__(self, *a, **k):
|
| 146 |
+
pass
|
| 147 |
+
locks = []
|
| 148 |
+
monkeypatch.setattr(app, "_payg_lock", lambda *a, **k: locks.append(a) or None)
|
| 149 |
+
log = ["Round 1: AI score 100% → 12% (rewrote 20 of 21 sentences)"]
|
| 150 |
+
r = dict(_res(100, 12, log, new="Changed text here."), zero_goal=True, left=2)
|
| 151 |
+
rep = Rep()
|
| 152 |
+
status = app._smart_result(r, 0.8, 0.5, False, rep, "paper.docx")[0]
|
| 153 |
+
assert "100% → 12%" in status and "2 sentence(s) still read as AI-written" in status
|
| 154 |
+
assert "No charge for this document" in status
|
| 155 |
+
assert rep.meta.get("no_charge") is True and not locks # nothing to pay, no credit used
|
| 156 |
+
rep2 = Rep()
|
| 157 |
+
rep2.meta = {}
|
| 158 |
+
app._smart_result(dict(_res(100, 0, log, new="Changed text here."), zero_goal=True, left=0), 0.8, 0.5, False,
|
| 159 |
+
rep2, "paper.docx")
|
| 160 |
+
assert len(locks) == 1 and not rep2.meta.get("no_charge") # at 0%: charged as usual
|