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>

Files changed (3) hide show
  1. app.py +14 -2
  2. fast.py +26 -14
  3. 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
- threading.Thread(target=F.selftest, daemon=True).start()
 
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
- # The whole job (reading, scoring, rewriting, saving) ends within FAST_BUDGET seconds, whatever the length: requests
42
- # still running at the deadline are dropped and their sentences keep their best version so far.
43
- BUDGET = float(os.getenv("FAST_BUDGET", "52"))
 
 
 
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", "4"))
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 deadline
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: # not enough time for another useful round (late requests are
407
- # dropped at the deadline, so a round may run short)
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.75 * (rnd - 1) / ROUNDS
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.75 / ROUNDS * 0.6, f"Round {rnd}: choosing the most natural version of each sentence")
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
- elif stats["pct"] > best[0]: # this round made it worse: the next one starts from the best
 
 
 
 
 
 
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