Spaces:
Running
Running
Upload 3 files
Browse files- app.py +27 -10
- report_flags.py +175 -0
- smart.py +29 -2
app.py
CHANGED
|
@@ -26,6 +26,7 @@ import dashboard as DB
|
|
| 26 |
import plans as P
|
| 27 |
import time
|
| 28 |
import site_pages as SP
|
|
|
|
| 29 |
from humanizer import BRAND, RAINBOW, LOGO_SRC, FAVICON_SRC, FAVICON_PATH, MODES, OUT_DIR
|
| 30 |
|
| 31 |
APP_NAME = os.getenv("APP_NAME", "CS AI Writing Suite")
|
|
@@ -258,8 +259,19 @@ def _smart_result(res, hi, mod, text_mode, rep=None, name=""):
|
|
| 258 |
return tuple(out)
|
| 259 |
|
| 260 |
|
| 261 |
-
def
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
text = (text or "").strip()
|
| 264 |
if not text:
|
| 265 |
raise gr.Error("Please paste some text.")
|
|
@@ -267,23 +279,23 @@ def _smart_text_core(text, target, passes, temp, mode, hi, mod, full_ai, vocab,
|
|
| 267 |
return _smart_result(res, hi, mod, True, rep, "Pasted text")
|
| 268 |
|
| 269 |
|
| 270 |
-
def _smart_docx_core(path, target, passes, temp, mode, hi, mod, full_ai, vocab, rep):
|
| 271 |
-
H.FULL_AI.set(bool(full_ai)); H.VOCAB_TARGET.set(float(vocab or 0))
|
| 272 |
if not path or not str(path).lower().endswith(".docx"):
|
| 273 |
raise gr.Error("Please upload a Word (.docx) file.")
|
| 274 |
res = S.smart_docx(path, target, passes, temp, mode, hi, mod, rep)
|
| 275 |
return _smart_result(res, hi, mod, False, rep, os.path.basename(str(path)))
|
| 276 |
|
| 277 |
|
| 278 |
-
def smart_text(text, target, passes, temp, mode, hi, mod, full_ai, vocab, user, request: gr.Request = None):
|
| 279 |
passes = P.max_passes(user, passes)
|
| 280 |
-
yield from _stream(_smart_text_core, (text, target, passes, temp, mode, hi, mod, bool(full_ai), vocab), 6, 0, user, request=request,
|
| 281 |
tool="Smart Humanize", paste=text)
|
| 282 |
|
| 283 |
|
| 284 |
-
def smart_docx(path, target, passes, temp, mode, hi, mod, full_ai, vocab, user, request: gr.Request = None):
|
| 285 |
passes = P.max_passes(user, passes)
|
| 286 |
-
yield from _stream(_smart_docx_core, (path, target, passes, temp, mode, hi, mod, bool(full_ai), vocab), 5, 0, user, True, request,
|
| 287 |
tool="Smart Humanize", words=P.doc_words(path) if path else None)
|
| 288 |
|
| 289 |
|
|
@@ -1222,6 +1234,11 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
|
|
| 1222 |
"and checked by Groq. Slower. "
|
| 1223 |
"Leave it off for your own writing or lightly edited AI text.",
|
| 1224 |
elem_classes="cs-fullai")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1225 |
s_hi, s_mod = thresholds()
|
| 1226 |
with gr.Tabs(elem_classes="sub-tabs"):
|
| 1227 |
with gr.Tab("✍️ Paste text · free"):
|
|
@@ -1685,9 +1702,9 @@ with gr.Blocks(title=f"{APP_NAME} · CS Software Solutions", theme=theme, css=CS
|
|
| 1685 |
for _cb, _sl in ((s_full, s_vocab), (hp_full, hp_vocab), (hd_full, hd_vocab)):
|
| 1686 |
_cb.change(lambda v: gr.update(value=70 if v else 0), _cb, _sl, queue=False, **HID)
|
| 1687 |
sp_in.change(count_words_for, [sp_in, user_state], sp_count, queue=False, **HID)
|
| 1688 |
-
sp_run = sp_btn.click(lambda: WAIT, None, sp_status, queue=False, **HID).then(smart_text, [sp_in, s_target, s_passes, s_temp, s_mode, s_hi, s_mod, s_full, s_vocab, user_state],
|
| 1689 |
[sp_status, sp_out, sp_prev, sp_scores, sp_file, sp_dl], **JOB(sp_status))
|
| 1690 |
-
sd_run = sd_btn.click(lambda: WAIT, None, sd_status, queue=False, **HID).then(smart_docx, [sd_in, s_target, s_passes, s_temp, s_mode, s_hi, s_mod, s_full, s_vocab, user_state],
|
| 1691 |
[sd_status, sd_prev, sd_scores, sd_file, sd_dl], **JOB(sd_status))
|
| 1692 |
|
| 1693 |
# detector
|
|
|
|
| 26 |
import plans as P
|
| 27 |
import time
|
| 28 |
import site_pages as SP
|
| 29 |
+
import report_flags as RF
|
| 30 |
from humanizer import BRAND, RAINBOW, LOGO_SRC, FAVICON_SRC, FAVICON_PATH, MODES, OUT_DIR
|
| 31 |
|
| 32 |
APP_NAME = os.getenv("APP_NAME", "CS AI Writing Suite")
|
|
|
|
| 259 |
return tuple(out)
|
| 260 |
|
| 261 |
|
| 262 |
+
def _use_report(report):
|
| 263 |
+
"""Uploaded AI / similarity report -> only the sentences it flags are rewritten (None = normal run)."""
|
| 264 |
+
if not report:
|
| 265 |
+
RF.ACTIVE.set(None)
|
| 266 |
+
return
|
| 267 |
+
try:
|
| 268 |
+
RF.ACTIVE.set(RF.load(report))
|
| 269 |
+
except RF.ReportError as err:
|
| 270 |
+
raise gr.Error(str(err))
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def _smart_text_core(text, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rep):
|
| 274 |
+
H.FULL_AI.set(bool(full_ai)); H.VOCAB_TARGET.set(float(vocab or 0)); _use_report(report)
|
| 275 |
text = (text or "").strip()
|
| 276 |
if not text:
|
| 277 |
raise gr.Error("Please paste some text.")
|
|
|
|
| 279 |
return _smart_result(res, hi, mod, True, rep, "Pasted text")
|
| 280 |
|
| 281 |
|
| 282 |
+
def _smart_docx_core(path, target, passes, temp, mode, hi, mod, full_ai, vocab, report, rep):
|
| 283 |
+
H.FULL_AI.set(bool(full_ai)); H.VOCAB_TARGET.set(float(vocab or 0)); _use_report(report)
|
| 284 |
if not path or not str(path).lower().endswith(".docx"):
|
| 285 |
raise gr.Error("Please upload a Word (.docx) file.")
|
| 286 |
res = S.smart_docx(path, target, passes, temp, mode, hi, mod, rep)
|
| 287 |
return _smart_result(res, hi, mod, False, rep, os.path.basename(str(path)))
|
| 288 |
|
| 289 |
|
| 290 |
+
def smart_text(text, target, passes, temp, mode, hi, mod, full_ai, vocab, report, user, request: gr.Request = None):
|
| 291 |
passes = P.max_passes(user, passes)
|
| 292 |
+
yield from _stream(_smart_text_core, (text, target, passes, temp, mode, hi, mod, bool(full_ai), vocab, report), 6, 0, user, request=request,
|
| 293 |
tool="Smart Humanize", paste=text)
|
| 294 |
|
| 295 |
|
| 296 |
+
def smart_docx(path, target, passes, temp, mode, hi, mod, full_ai, vocab, report, user, request: gr.Request = None):
|
| 297 |
passes = P.max_passes(user, passes)
|
| 298 |
+
yield from _stream(_smart_docx_core, (path, target, passes, temp, mode, hi, mod, bool(full_ai), vocab, report), 5, 0, user, True, request,
|
| 299 |
tool="Smart Humanize", words=P.doc_words(path) if path else None)
|
| 300 |
|
| 301 |
|
|
|
|
| 1234 |
"and checked by Groq. Slower. "
|
| 1235 |
"Leave it off for your own writing or lightly edited AI text.",
|
| 1236 |
elem_classes="cs-fullai")
|
| 1237 |
+
s_report = gr.File(label="📑 Have an AI or similarity report? Upload it (optional)", file_types=[".pdf", ".docx"],
|
| 1238 |
+
type="filepath", elem_classes="cs-report")
|
| 1239 |
+
gr.HTML('<div class="cs-gauge-cap" style="text-align:left;margin:-4px 2px 4px">Turnitin, iThenticate, GPTZero, '
|
| 1240 |
+
'ZeroGPT, Copyleaks or any other checker: upload the report PDF as downloaded. Only the sentences '
|
| 1241 |
+
'the report highlights are rewritten; everything else stays exactly as it is.</div>')
|
| 1242 |
s_hi, s_mod = thresholds()
|
| 1243 |
with gr.Tabs(elem_classes="sub-tabs"):
|
| 1244 |
with gr.Tab("✍️ Paste text · free"):
|
|
|
|
| 1702 |
for _cb, _sl in ((s_full, s_vocab), (hp_full, hp_vocab), (hd_full, hd_vocab)):
|
| 1703 |
_cb.change(lambda v: gr.update(value=70 if v else 0), _cb, _sl, queue=False, **HID)
|
| 1704 |
sp_in.change(count_words_for, [sp_in, user_state], sp_count, queue=False, **HID)
|
| 1705 |
+
sp_run = sp_btn.click(lambda: WAIT, None, sp_status, queue=False, **HID).then(smart_text, [sp_in, s_target, s_passes, s_temp, s_mode, s_hi, s_mod, s_full, s_vocab, s_report, user_state],
|
| 1706 |
[sp_status, sp_out, sp_prev, sp_scores, sp_file, sp_dl], **JOB(sp_status))
|
| 1707 |
+
sd_run = sd_btn.click(lambda: WAIT, None, sd_status, queue=False, **HID).then(smart_docx, [sd_in, s_target, s_passes, s_temp, s_mode, s_hi, s_mod, s_full, s_vocab, s_report, user_state],
|
| 1708 |
[sd_status, sd_prev, sd_scores, sd_file, sd_dl], **JOB(sd_status))
|
| 1709 |
|
| 1710 |
# detector
|
report_flags.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Report-guided rewriting: read an AI-detection or similarity report (Turnitin, iThenticate, GPTZero, ZeroGPT,
|
| 3 |
+
Copyleaks, our own CS report, ...) and find the passages it FLAGS, whatever the tool. A passage counts as flagged
|
| 4 |
+
when the report marks it in any of the usual ways:
|
| 5 |
+
|
| 6 |
+
* highlight / underline / squiggly annotations (PDF)
|
| 7 |
+
* coloured text (similarity reports colour matched text by source)
|
| 8 |
+
* coloured shading drawn behind the text (AI-writing reports shade flagged sentences)
|
| 9 |
+
* Word reports (.docx): highlighted, shaded or coloured runs
|
| 10 |
+
|
| 11 |
+
The flagged words are then matched to the sentences of the customer's document by word overlap (line breaks,
|
| 12 |
+
hyphenation and page furniture in the report do not matter). Smart Humanize then rewrites exactly those
|
| 13 |
+
sentences and nothing else.
|
| 14 |
+
"""
|
| 15 |
+
import contextvars
|
| 16 |
+
import re
|
| 17 |
+
|
| 18 |
+
MIN_SHARE = 0.5 # a sentence is flagged when at least half of its word trigrams were marked in the report
|
| 19 |
+
_WORD = re.compile(r"[A-Za-z0-9]+(?:['’][A-Za-z]+)?")
|
| 20 |
+
|
| 21 |
+
# set per job by the app when a report is uploaded (None = no report: normal Smart Humanize)
|
| 22 |
+
ACTIVE = contextvars.ContextVar("report_flags", default=None)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ReportError(Exception):
|
| 26 |
+
"""The report could not be used (no flagged passages found, scanned image, wrong file)."""
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _norm_words(text):
|
| 30 |
+
text = re.sub(r"(\w)-\s*\n\s*(\w)", r"\1\2", text or "") # de-hyphenate line breaks
|
| 31 |
+
return [w.lower().replace("’", "'") for w in _WORD.findall(text)]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _trigrams(words):
|
| 35 |
+
return {tuple(words[i:i + 3]) for i in range(len(words) - 2)}
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _colourful(rgb, min_sat=0.12):
|
| 39 |
+
"""True for a real colour (not black, white or grey)."""
|
| 40 |
+
if not rgb:
|
| 41 |
+
return False
|
| 42 |
+
r, g, b = rgb[:3]
|
| 43 |
+
return max(r, g, b) - min(r, g, b) >= min_sat
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _int_rgb(c):
|
| 47 |
+
return ((c >> 16) & 255) / 255, ((c >> 8) & 255) / 255, (c & 255) / 255
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _pdf_runs(path):
|
| 51 |
+
"""Runs of flagged words from a PDF report (each run = consecutive marked words, in reading order)."""
|
| 52 |
+
import fitz # PyMuPDF
|
| 53 |
+
try:
|
| 54 |
+
pdf = fitz.open(path)
|
| 55 |
+
except Exception as err:
|
| 56 |
+
raise ReportError(f"Could not open the report: {err}")
|
| 57 |
+
runs, n_words = [], 0
|
| 58 |
+
for page in pdf:
|
| 59 |
+
words = page.get_text("words") # x0, y0, x1, y1, text, block, line, word_no
|
| 60 |
+
n_words += len(words)
|
| 61 |
+
if not words:
|
| 62 |
+
continue
|
| 63 |
+
area = page.rect.width * page.rect.height
|
| 64 |
+
marks = [] # rectangles that mark text
|
| 65 |
+
for a in page.annots() or []:
|
| 66 |
+
if a.type[0] in (8, 9, 10): # highlight, underline, squiggly
|
| 67 |
+
v = a.vertices or []
|
| 68 |
+
if len(v) >= 4:
|
| 69 |
+
for i in range(0, len(v) - 3, 4):
|
| 70 |
+
marks.append(fitz.Quad(v[i:i + 4]).rect)
|
| 71 |
+
else:
|
| 72 |
+
marks.append(a.rect)
|
| 73 |
+
for d in page.get_drawings(): # shading drawn behind flagged text
|
| 74 |
+
fill = d.get("fill")
|
| 75 |
+
r = d.get("rect")
|
| 76 |
+
if fill and r is not None and _colourful(fill, 0.08) and r.width * r.height < 0.5 * area \
|
| 77 |
+
and r.height < 60:
|
| 78 |
+
marks.append(r)
|
| 79 |
+
coloured = set() # coloured text spans (similarity reports)
|
| 80 |
+
for blk in page.get_text("dict").get("blocks", []):
|
| 81 |
+
for line in blk.get("lines", []):
|
| 82 |
+
for sp in line.get("spans", []):
|
| 83 |
+
if sp.get("text", "").strip() and _colourful(_int_rgb(sp.get("color", 0))):
|
| 84 |
+
coloured.add(tuple(round(x, 1) for x in sp["bbox"]))
|
| 85 |
+
col_rects = [fitz.Rect(b) for b in coloured]
|
| 86 |
+
|
| 87 |
+
def marked(w):
|
| 88 |
+
cx, cy = (w[0] + w[2]) / 2, (w[1] + w[3]) / 2
|
| 89 |
+
p = fitz.Point(cx, cy)
|
| 90 |
+
return any(r.contains(p) for r in marks) or any(r.contains(p) for r in col_rects)
|
| 91 |
+
|
| 92 |
+
cur = []
|
| 93 |
+
for w in sorted(words, key=lambda w: (w[5], w[6], w[7])):
|
| 94 |
+
if marked(w):
|
| 95 |
+
cur.append(w[4])
|
| 96 |
+
elif cur:
|
| 97 |
+
runs.append(" ".join(cur))
|
| 98 |
+
cur = []
|
| 99 |
+
if cur:
|
| 100 |
+
runs.append(" ".join(cur))
|
| 101 |
+
if n_words == 0:
|
| 102 |
+
raise ReportError("This report has no readable text (it looks like a scan or image). Download the report "
|
| 103 |
+
"as a PDF from the checker's website and upload that file.")
|
| 104 |
+
return runs
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _docx_runs(path):
|
| 108 |
+
"""Runs of flagged text from a Word report: highlighted, shaded or coloured runs."""
|
| 109 |
+
from docx import Document
|
| 110 |
+
from docx.oxml.ns import qn
|
| 111 |
+
runs = []
|
| 112 |
+
for p in Document(path).paragraphs:
|
| 113 |
+
cur = []
|
| 114 |
+
for r in p.runs:
|
| 115 |
+
rpr = r._r.find(qn("w:rPr"))
|
| 116 |
+
flag = False
|
| 117 |
+
if rpr is not None:
|
| 118 |
+
hl = rpr.find(qn("w:highlight"))
|
| 119 |
+
shd = rpr.find(qn("w:shd"))
|
| 120 |
+
col = rpr.find(qn("w:color"))
|
| 121 |
+
flag = (hl is not None and hl.get(qn("w:val")) not in (None, "none")) or \
|
| 122 |
+
(shd is not None and (shd.get(qn("w:fill")) or "auto").lower() not in ("auto", "ffffff")) or \
|
| 123 |
+
(col is not None and _colourful(_hex_rgb(col.get(qn("w:val")))))
|
| 124 |
+
if flag and r.text.strip():
|
| 125 |
+
cur.append(r.text)
|
| 126 |
+
elif cur:
|
| 127 |
+
runs.append(" ".join(cur))
|
| 128 |
+
cur = []
|
| 129 |
+
if cur:
|
| 130 |
+
runs.append(" ".join(cur))
|
| 131 |
+
return runs
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _hex_rgb(h):
|
| 135 |
+
try:
|
| 136 |
+
h = (h or "").strip()
|
| 137 |
+
return int(h[0:2], 16) / 255, int(h[2:4], 16) / 255, int(h[4:6], 16) / 255
|
| 138 |
+
except Exception:
|
| 139 |
+
return None
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class Flags:
|
| 143 |
+
"""The flagged passages of one report; .match(sentence) says whether a document sentence was flagged."""
|
| 144 |
+
|
| 145 |
+
def __init__(self, runs):
|
| 146 |
+
self.runs = [r for r in runs if len(_norm_words(r)) >= 3]
|
| 147 |
+
self.tri = set()
|
| 148 |
+
for r in self.runs:
|
| 149 |
+
self.tri |= _trigrams(_norm_words(r))
|
| 150 |
+
self.matched = 0
|
| 151 |
+
|
| 152 |
+
def match(self, sentence):
|
| 153 |
+
t = _trigrams(_norm_words(sentence))
|
| 154 |
+
if not t:
|
| 155 |
+
return False
|
| 156 |
+
hit = sum(1 for x in t if x in self.tri) / len(t) >= MIN_SHARE
|
| 157 |
+
self.matched += hit
|
| 158 |
+
return hit
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def load(path):
|
| 162 |
+
"""Read a report file -> Flags. Raises ReportError with a customer-friendly message."""
|
| 163 |
+
p = str(path or "")
|
| 164 |
+
low = p.lower()
|
| 165 |
+
if low.endswith(".pdf"):
|
| 166 |
+
runs = _pdf_runs(p)
|
| 167 |
+
elif low.endswith(".docx"):
|
| 168 |
+
runs = _docx_runs(p)
|
| 169 |
+
else:
|
| 170 |
+
raise ReportError("Please upload the report as a PDF (or a Word .docx with the flagged text highlighted).")
|
| 171 |
+
flags = Flags(runs)
|
| 172 |
+
if not flags.tri:
|
| 173 |
+
raise ReportError("No highlighted or flagged passages were found in this report. Upload the full report PDF "
|
| 174 |
+
"as downloaded from the checker (the version with the coloured / highlighted text).")
|
| 175 |
+
return flags
|
smart.py
CHANGED
|
@@ -19,6 +19,7 @@ import detector as D
|
|
| 19 |
import textparse as TP
|
| 20 |
import docx_report as DX
|
| 21 |
import humanizer as H
|
|
|
|
| 22 |
|
| 23 |
W_T = qn("w:t")
|
| 24 |
_XML_SPACE = "{http://www.w3.org/XML/1998/namespace}space"
|
|
@@ -191,6 +192,12 @@ def _target_change():
|
|
| 191 |
|
| 192 |
|
| 193 |
def _done_early(k, pct, target, chg):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
"""Normal mode: stop when the AI score is 0 or (from pass 2) at/below the target.
|
| 195 |
100% AI mode: at least one rewrite, and the new-wording target must also be reached."""
|
| 196 |
if not _all_sentences():
|
|
@@ -204,6 +211,8 @@ def _better(cand, best, k):
|
|
| 204 |
then the lowest AI score; below the target, the version with the most new wording wins."""
|
| 205 |
if best is None:
|
| 206 |
return True
|
|
|
|
|
|
|
| 207 |
if not _all_sentences():
|
| 208 |
return cand[0] < best[0] or (k > 1 and cand[0] == best[0])
|
| 209 |
if best[1] is None: # best is still the untouched original
|
|
@@ -275,6 +284,9 @@ def smart_docx(path, target, passes, temperature, mode, hi, mod, rep):
|
|
| 275 |
log, first, note = [], None, ""
|
| 276 |
orig_text = "\n".join(b.text for b in D.docx_blocks(doc) if b.kind == "para")
|
| 277 |
TP.GLOSSARY.set(TP.build_glossary([orig_text])) # the document's key terms stay untouched
|
|
|
|
|
|
|
|
|
|
| 278 |
best = None # (pct, docx bytes, scored blocks)
|
| 279 |
step = 0.8 / max(1, passes)
|
| 280 |
for k in range(1, int(passes) + 2): # last iteration = final check only
|
|
@@ -299,12 +311,18 @@ def smart_docx(path, target, passes, temperature, mode, hi, mod, rep):
|
|
| 299 |
if b.kind != "para" or b.el is None:
|
| 300 |
continue
|
| 301 |
for sp, s, p in zip(b.cspans, b.sents, b.scores):
|
| 302 |
-
if
|
|
|
|
| 303 |
sub = safe_subspan(b.el, *sp)
|
| 304 |
if sub:
|
| 305 |
jobs.append((b, sub, b.raw[sub[0]:sub[1]]))
|
| 306 |
else:
|
| 307 |
locked += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
if not jobs:
|
| 309 |
log.append(_log_line(k, stats["pct"]) + f" — no rewritable sentences left ({locked} locked)")
|
| 310 |
break
|
|
@@ -359,6 +377,9 @@ def smart_text(text, target, passes, temperature, mode, hi, mod, rep):
|
|
| 359 |
paras = [p for p, _ in parts]
|
| 360 |
orig = TP.join_paste(parts)
|
| 361 |
log, first, note, best = [], None, "", None # best = (pct, paras, blocks)
|
|
|
|
|
|
|
|
|
|
| 362 |
step = 0.8 / max(1, passes)
|
| 363 |
for k in range(1, int(passes) + 2):
|
| 364 |
base = 0.05 + step * (k - 1)
|
|
@@ -377,7 +398,13 @@ def smart_text(text, target, passes, temperature, mode, hi, mod, rep):
|
|
| 377 |
break
|
| 378 |
full = _all_sentences() # 100% AI box / vocabulary level: every sentence
|
| 379 |
jobs = [(bi, si, s) for bi, b in enumerate(blocks) if b.kind == "para"
|
| 380 |
-
for si, (s, p) in enumerate(zip(b.sents, b.scores))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
if not jobs:
|
| 382 |
log.append(_log_line(k, stats["pct"]) + " — no rewritable sentences left")
|
| 383 |
break
|
|
|
|
| 19 |
import textparse as TP
|
| 20 |
import docx_report as DX
|
| 21 |
import humanizer as H
|
| 22 |
+
import report_flags as R
|
| 23 |
|
| 24 |
W_T = qn("w:t")
|
| 25 |
_XML_SPACE = "{http://www.w3.org/XML/1998/namespace}space"
|
|
|
|
| 192 |
|
| 193 |
|
| 194 |
def _done_early(k, pct, target, chg):
|
| 195 |
+
if R.ACTIVE.get() is not None: # report-guided: always rewrite what the report flagged
|
| 196 |
+
return False
|
| 197 |
+
return _done_early_inner(k, pct, target, chg)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def _done_early_inner(k, pct, target, chg):
|
| 201 |
"""Normal mode: stop when the AI score is 0 or (from pass 2) at/below the target.
|
| 202 |
100% AI mode: at least one rewrite, and the new-wording target must also be reached."""
|
| 203 |
if not _all_sentences():
|
|
|
|
| 211 |
then the lowest AI score; below the target, the version with the most new wording wins."""
|
| 212 |
if best is None:
|
| 213 |
return True
|
| 214 |
+
if R.ACTIVE.get() is not None: # report-guided: the rewritten version is the result
|
| 215 |
+
return k > 1
|
| 216 |
if not _all_sentences():
|
| 217 |
return cand[0] < best[0] or (k > 1 and cand[0] == best[0])
|
| 218 |
if best[1] is None: # best is still the untouched original
|
|
|
|
| 284 |
log, first, note = [], None, ""
|
| 285 |
orig_text = "\n".join(b.text for b in D.docx_blocks(doc) if b.kind == "para")
|
| 286 |
TP.GLOSSARY.set(TP.build_glossary([orig_text])) # the document's key terms stay untouched
|
| 287 |
+
rpt = R.ACTIVE.get() # uploaded AI / similarity report: rewrite only what it flags
|
| 288 |
+
if rpt is not None:
|
| 289 |
+
passes = 1
|
| 290 |
best = None # (pct, docx bytes, scored blocks)
|
| 291 |
step = 0.8 / max(1, passes)
|
| 292 |
for k in range(1, int(passes) + 2): # last iteration = final check only
|
|
|
|
| 311 |
if b.kind != "para" or b.el is None:
|
| 312 |
continue
|
| 313 |
for sp, s, p in zip(b.cspans, b.sents, b.scores):
|
| 314 |
+
pick = rpt.match(s) if rpt is not None else (full or p >= mod)
|
| 315 |
+
if sp and pick and len(s.split()) >= 5:
|
| 316 |
sub = safe_subspan(b.el, *sp)
|
| 317 |
if sub:
|
| 318 |
jobs.append((b, sub, b.raw[sub[0]:sub[1]]))
|
| 319 |
else:
|
| 320 |
locked += 1
|
| 321 |
+
if not jobs and rpt is not None:
|
| 322 |
+
raise gr.Error("None of the sentences in your document match the passages flagged in the report. "
|
| 323 |
+
"Check that the report belongs to this document.")
|
| 324 |
+
if rpt is not None:
|
| 325 |
+
log.append(f"Report: {len(jobs) + locked} of your sentences match passages the report flagged")
|
| 326 |
if not jobs:
|
| 327 |
log.append(_log_line(k, stats["pct"]) + f" — no rewritable sentences left ({locked} locked)")
|
| 328 |
break
|
|
|
|
| 377 |
paras = [p for p, _ in parts]
|
| 378 |
orig = TP.join_paste(parts)
|
| 379 |
log, first, note, best = [], None, "", None # best = (pct, paras, blocks)
|
| 380 |
+
rpt = R.ACTIVE.get() # uploaded AI / similarity report: rewrite only what it flags
|
| 381 |
+
if rpt is not None:
|
| 382 |
+
passes = 1
|
| 383 |
step = 0.8 / max(1, passes)
|
| 384 |
for k in range(1, int(passes) + 2):
|
| 385 |
base = 0.05 + step * (k - 1)
|
|
|
|
| 398 |
break
|
| 399 |
full = _all_sentences() # 100% AI box / vocabulary level: every sentence
|
| 400 |
jobs = [(bi, si, s) for bi, b in enumerate(blocks) if b.kind == "para"
|
| 401 |
+
for si, (s, p) in enumerate(zip(b.sents, b.scores))
|
| 402 |
+
if (rpt.match(s) if rpt is not None else (full or p >= mod)) and len(s.split()) >= 5]
|
| 403 |
+
if rpt is not None:
|
| 404 |
+
if not jobs:
|
| 405 |
+
raise gr.Error("None of your sentences match the passages flagged in the report. "
|
| 406 |
+
"Check that the report belongs to this text.")
|
| 407 |
+
log.append(f"Report: {len(jobs)} of your sentences match passages the report flagged")
|
| 408 |
if not jobs:
|
| 409 |
log.append(_log_line(k, stats["pct"]) + " — no rewritable sentences left")
|
| 410 |
break
|