Spaces:
Running
Running
unknown Claude Opus 5.5 commited on
Commit ·
eebf465
1
Parent(s): f85f16b
Stronger AI detector: paraphrase-robust third model in the ensemble
Browse filesfakespot-ai/roberta-base-ai-text-detection-v1 averaged with the current coai + desklib score. Measured
(devtools/compare_detectors.py): AI samples 18/18 flagged, human abstracts from 2008 0/21, paragraphs our own
humanizer had brought to 6%: 6/16 flagged (was 1/16). RADAR was tested and rejected (separation 0.72 on academic
text). DETECTOR_MODEL3="" turns it off; DETECTOR_WEIGHT3 sets its share (0.5).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- detector.py +52 -3
- devtools/compare_detectors.py +2 -0
detector.py
CHANGED
|
@@ -54,6 +54,42 @@ WEIGHT2 = float(os.getenv("DETECTOR_WEIGHT2", "0.7"))
|
|
| 54 |
COMBINE = os.getenv("DETECTOR_COMBINE", "mean").strip().lower()
|
| 55 |
_tok2, _model2, _m2_failed = None, None, False
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
def _desklib_class():
|
| 59 |
import torch
|
|
@@ -139,6 +175,7 @@ def load_model():
|
|
| 139 |
if os.getenv("SKIP_MODEL_LOAD") != "1":
|
| 140 |
load_model()
|
| 141 |
load_model2()
|
|
|
|
| 142 |
DX.start_lo_init() # fetch newest LibreOffice in the background
|
| 143 |
|
| 144 |
def _score_with(tok, model, texts):
|
|
@@ -162,7 +199,9 @@ def _score_gpu(texts):
|
|
| 162 |
p1 = _score_with(tok, model, texts)
|
| 163 |
tok2, model2 = load_model2()
|
| 164 |
p2 = _score_with2(tok2, model2, texts) if model2 is not None else None
|
| 165 |
-
|
|
|
|
|
|
|
| 166 |
|
| 167 |
|
| 168 |
_cpu_det, _cpu_det_lock = None, __import__("threading").Lock()
|
|
@@ -170,7 +209,7 @@ LAST_ENGINE = "GPU"
|
|
| 170 |
|
| 171 |
|
| 172 |
def _score_cpu(texts):
|
| 173 |
-
global _cpu_det
|
| 174 |
import torch
|
| 175 |
from transformers import AutoModelForSequenceClassification
|
| 176 |
with _cpu_det_lock:
|
|
@@ -185,7 +224,17 @@ def _score_cpu(texts):
|
|
| 185 |
if next(model2.parameters()).device.type != "cpu":
|
| 186 |
model2 = _cpu_model2()
|
| 187 |
p2 = _score_with2(tok2, model2, texts)
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
|
| 191 |
_cpu_m2 = None
|
|
|
|
| 54 |
COMBINE = os.getenv("DETECTOR_COMBINE", "mean").strip().lower()
|
| 55 |
_tok2, _model2, _m2_failed = None, None, False
|
| 56 |
|
| 57 |
+
# Third detector: robust to paraphrasing (what Turnitin's "AI-paraphrased" check is about). Measured with
|
| 58 |
+
# devtools/compare_detectors.py on 18 AI-written academic samples, 21 human abstracts from 2008 and 16 paragraphs our
|
| 59 |
+
# own humanizer had brought to 6%: coai + desklib flagged 18/18 AI, 0/21 human, 1/16 humanized; this RoBERTa
|
| 60 |
+
# (fakespot-ai) flagged 18/18, 2/21, 8/16. Averaged with the first two: 18/18 AI, 0/21 human, 6/16 humanized, so
|
| 61 |
+
# the score is much harder to game without flagging human writing.
|
| 62 |
+
# DETECTOR_MODEL3 default fakespot-ai/roberta-base-ai-text-detection-v1 ("" = off)
|
| 63 |
+
# DETECTOR_WEIGHT3 default 0.5 share of the final score
|
| 64 |
+
MODEL3_ID = os.getenv("DETECTOR_MODEL3", "fakespot-ai/roberta-base-ai-text-detection-v1").strip()
|
| 65 |
+
WEIGHT3 = float(os.getenv("DETECTOR_WEIGHT3", "0.5"))
|
| 66 |
+
_tok3, _model3, _m3_failed = None, None, False
|
| 67 |
+
_cpu_m3 = None
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def load_model3():
|
| 71 |
+
global _tok3, _model3, _m3_failed
|
| 72 |
+
if not MODEL3_ID or _m3_failed or _model3 is not None:
|
| 73 |
+
return _tok3, _model3
|
| 74 |
+
try:
|
| 75 |
+
import torch
|
| 76 |
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
| 77 |
+
_tok3 = AutoTokenizer.from_pretrained(MODEL3_ID, token=HF_TOKEN)
|
| 78 |
+
_model3 = AutoModelForSequenceClassification.from_pretrained(MODEL3_ID, token=HF_TOKEN).eval()
|
| 79 |
+
if ZERO or torch.cuda.is_available():
|
| 80 |
+
_model3.to("cuda")
|
| 81 |
+
print(f"[detector] third model loaded: {MODEL3_ID}", flush=True)
|
| 82 |
+
except Exception as err:
|
| 83 |
+
_m3_failed = True
|
| 84 |
+
print(f"[detector] third model not available ({err}); first two only", flush=True)
|
| 85 |
+
return _tok3, _model3
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _with3(p12, p3):
|
| 89 |
+
if p3 is None:
|
| 90 |
+
return p12
|
| 91 |
+
return [(1 - WEIGHT3) * a + WEIGHT3 * b for a, b in zip(p12, p3)]
|
| 92 |
+
|
| 93 |
|
| 94 |
def _desklib_class():
|
| 95 |
import torch
|
|
|
|
| 175 |
if os.getenv("SKIP_MODEL_LOAD") != "1":
|
| 176 |
load_model()
|
| 177 |
load_model2()
|
| 178 |
+
load_model3()
|
| 179 |
DX.start_lo_init() # fetch newest LibreOffice in the background
|
| 180 |
|
| 181 |
def _score_with(tok, model, texts):
|
|
|
|
| 199 |
p1 = _score_with(tok, model, texts)
|
| 200 |
tok2, model2 = load_model2()
|
| 201 |
p2 = _score_with2(tok2, model2, texts) if model2 is not None else None
|
| 202 |
+
tok3, model3 = load_model3()
|
| 203 |
+
p3 = _score_with(tok3, model3, texts) if model3 is not None else None # label 1 = "ai"
|
| 204 |
+
return _with3(_combine(p1, p2), p3)
|
| 205 |
|
| 206 |
|
| 207 |
_cpu_det, _cpu_det_lock = None, __import__("threading").Lock()
|
|
|
|
| 209 |
|
| 210 |
|
| 211 |
def _score_cpu(texts):
|
| 212 |
+
global _cpu_det, _cpu_m3
|
| 213 |
import torch
|
| 214 |
from transformers import AutoModelForSequenceClassification
|
| 215 |
with _cpu_det_lock:
|
|
|
|
| 224 |
if next(model2.parameters()).device.type != "cpu":
|
| 225 |
model2 = _cpu_model2()
|
| 226 |
p2 = _score_with2(tok2, model2, texts)
|
| 227 |
+
p3 = None
|
| 228 |
+
tok3, model3 = load_model3()
|
| 229 |
+
if model3 is not None:
|
| 230 |
+
if next(model3.parameters()).device.type != "cpu":
|
| 231 |
+
with _cpu_det_lock:
|
| 232 |
+
if _cpu_m3 is None:
|
| 233 |
+
from transformers import AutoModelForSequenceClassification as _A
|
| 234 |
+
_cpu_m3 = _A.from_pretrained(MODEL3_ID, token=HF_TOKEN).eval()
|
| 235 |
+
model3 = _cpu_m3
|
| 236 |
+
p3 = _score_with(tok3, model3, texts)
|
| 237 |
+
return _with3(_combine(p1, p2), p3)
|
| 238 |
|
| 239 |
|
| 240 |
_cpu_m2 = None
|
devtools/compare_detectors.py
CHANGED
|
@@ -60,6 +60,8 @@ def main():
|
|
| 60 |
scorers["current (live: coai + desklib)"] = D.score_texts
|
| 61 |
if "radar" in want:
|
| 62 |
scorers["RADAR (paraphrase-robust)"] = hf_scorer("TrustSafeAI/RADAR-Vicuna-7B", ai_index=0)
|
|
|
|
|
|
|
| 63 |
if "fakespot" in want:
|
| 64 |
scorers["fakespot roberta"] = hf_scorer("fakespot-ai/roberta-base-ai-text-detection-v1")
|
| 65 |
if "desklib" in want:
|
|
|
|
| 60 |
scorers["current (live: coai + desklib)"] = D.score_texts
|
| 61 |
if "radar" in want:
|
| 62 |
scorers["RADAR (paraphrase-robust)"] = hf_scorer("TrustSafeAI/RADAR-Vicuna-7B", ai_index=0)
|
| 63 |
+
if "superannotate" in want:
|
| 64 |
+
scorers["SuperAnnotate"] = hf_scorer("SuperAnnotate/ai-detector")
|
| 65 |
if "fakespot" in want:
|
| 66 |
scorers["fakespot roberta"] = hf_scorer("fakespot-ai/roberta-base-ai-text-detection-v1")
|
| 67 |
if "desklib" in want:
|