Spaces:
Sleeping
Sleeping
File size: 8,851 Bytes
8392b7d 019b6f4 8392b7d 019b6f4 8392b7d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | """AI text detection endpoints (Task 19).
Unit tests (SKIP_MODEL_LOAD=1, no torch): the detector routes read models from
app.state.model_cache via get_or_load_model, so client_with_detectors pre-seeds
that cache with FakeDetector instances. Wiring, validation, and disagreement are
checked over HTTP only.
Integration tests (@pytest.mark.integration, real weights, `ai` conda env)
exercise the two output adapters end to end: desklib's custom single-logit
sigmoid head and the softmax detectors' canonical label mapping.
"""
import pytest
from app.model_registry import ModelTask, models_for_task
from app.routes import DETECTOR_WARNING
# The exact warning the product promises on every detector response. A literal
# copy here guards against an accidental reword drifting from the spec.
EXPECTED_WARNING = (
"AI detectors are probabilistic and can be wrong, especially on short, "
"edited, non-native, highly formal, or mixed-authorship text. "
"Do not use this as proof of authorship."
)
def test_warning_constant_matches_spec():
# Guard the source constant itself, not just the response, against a reword.
assert DETECTOR_WARNING == EXPECTED_WARNING
# --- /api/ai-detect -------------------------------------------------------
def test_ai_detect_returns_result_and_warning(client_with_detectors):
resp = client_with_detectors.post("/api/ai-detect", json={"text": "some text"})
assert resp.status_code == 200
body = resp.json()
result = body["result"]
assert result["model_id"] == "desklib-ai-detector"
assert result["label"] in {"human", "ai"}
assert set(result["scores"]) == {"human", "ai"}
assert result["confidence"] == max(result["scores"].values())
assert body["warning"] == EXPECTED_WARNING
def test_ai_detect_defaults_to_desklib(client_with_detectors):
resp = client_with_detectors.post("/api/ai-detect", json={"text": "x"})
assert resp.json()["result"]["model_id"] == "desklib-ai-detector"
def test_ai_detect_honors_explicit_detector(client_with_detectors):
resp = client_with_detectors.post(
"/api/ai-detect", json={"text": "x", "model_ids": ["fakespot-ai-detector"]}
)
assert resp.json()["result"]["model_id"] == "fakespot-ai-detector"
def test_ai_detect_row_carries_registry_metadata(client_with_detectors):
result = client_with_detectors.post("/api/ai-detect", json={"text": "x"}).json()["result"]
assert result["name"] == "desklib/ai-text-detector-v1.01"
assert result["domain"]
assert result["note"]
assert isinstance(result["latency_ms"], (int, float))
assert result["latency_ms"] >= 0
def test_ai_detect_rejects_sentiment_model(client_with_detectors):
resp = client_with_detectors.post(
"/api/ai-detect", json={"text": "x", "model_ids": ["twitter-roberta"]}
)
assert resp.status_code == 400
assert "sentiment" in resp.json()["detail"].lower()
def test_ai_detect_rejects_unknown_model(client_with_detectors):
resp = client_with_detectors.post(
"/api/ai-detect", json={"text": "x", "model_ids": ["not-a-real-model"]}
)
assert resp.status_code == 400
assert "not-a-real-model" in resp.json()["detail"]
def test_ai_detect_rejects_disabled_model(monkeypatch, client):
# ENABLED_MODELS is the public-deploy allowlist. The guard runs BEFORE
# get_or_load_model, so no seeded cache entry is needed.
monkeypatch.setenv("ENABLED_MODELS", "oxidane-ai-detector")
resp = client.post(
"/api/ai-detect", json={"text": "x", "model_ids": ["desklib-ai-detector"]}
)
assert resp.status_code == 403
assert "disabled" in resp.json()["detail"].lower()
# --- /api/ai-detect/compare ----------------------------------------------
def test_compare_defaults_to_all_detectors(client_with_detectors):
resp = client_with_detectors.post("/api/ai-detect/compare", json={"text": "x"})
assert resp.status_code == 200
ids = {r["model_id"] for r in resp.json()["results"]}
assert ids == set(models_for_task(ModelTask.AI_TEXT_DETECTION))
def test_compare_disagreement_true_when_labels_differ(client_with_detectors):
# Seeded fakes: desklib+fakespot say "ai", oxidane says "human" → disagree.
body = client_with_detectors.post("/api/ai-detect/compare", json={"text": "x"}).json()
assert body["disagreement"] is True
assert body["warning"] == EXPECTED_WARNING
def test_compare_disagreement_false_when_labels_agree(client_with_detectors):
body = client_with_detectors.post(
"/api/ai-detect/compare",
json={"text": "x", "model_ids": ["desklib-ai-detector", "fakespot-ai-detector"]},
).json()
assert [r["label"] for r in body["results"]] == ["ai", "ai"]
assert body["disagreement"] is False
def test_compare_honors_explicit_model_ids(client_with_detectors):
resp = client_with_detectors.post(
"/api/ai-detect/compare",
json={"text": "x", "model_ids": ["oxidane-ai-detector"]},
)
assert [r["model_id"] for r in resp.json()["results"]] == ["oxidane-ai-detector"]
def test_compare_rejects_sentiment_model(client_with_detectors):
resp = client_with_detectors.post(
"/api/ai-detect/compare", json={"text": "x", "model_ids": ["finbert"]}
)
assert resp.status_code == 400
assert "sentiment" in resp.json()["detail"].lower()
def test_compare_dedupes_duplicate_detector_ids(client_with_detectors):
# Same dedupe guard as /api/compare: a repeated detector id collapses to one
# row, so one request can't queue the same detector many times over.
resp = client_with_detectors.post(
"/api/ai-detect/compare",
json={"text": "x", "model_ids": ["desklib-ai-detector", "desklib-ai-detector"]},
)
assert resp.status_code == 200
assert [r["model_id"] for r in resp.json()["results"]] == ["desklib-ai-detector"]
# --- cross-task rejection: detectors are refused by /api/compare ----------
def test_sentiment_compare_rejects_detector_model(client_with_detectors):
resp = client_with_detectors.post(
"/api/compare", json={"text": "x", "model_ids": ["desklib-ai-detector"]}
)
assert resp.status_code == 400
assert "sentiment" in resp.json()["detail"].lower()
# --- integration: real weights, real adapters ----------------------------
# From the desklib model card — an obviously AI-ish paragraph vs a terser,
# human-ish note. Labels on borderline text are noisy, so integration tests
# assert shape (keys, sum≈1, valid label), not a specific verdict.
AI_TEXT = (
"AI detection refers to the process of identifying whether a given piece of "
"content, such as text, images, or audio, has been generated by artificial "
"intelligence. This is achieved using various machine learning techniques, "
"including perplexity analysis, entropy measurements, linguistic pattern "
"recognition, and neural network classifiers trained on human and AI-generated "
"data. Advanced AI detection tools assess writing style, coherence, and "
"statistical properties to determine the likelihood of AI involvement."
)
HUMAN_TEXT = (
"It is estimated that a major part of the content in the internet will be "
"generated by AI / LLMs by 2025. This leads to a lot of misinformation and "
"credibility related issues. That is why if is important to have accurate "
"tools to identify if a content is AI generated or human written"
)
def _assert_detector_output(row: dict) -> None:
assert set(row["scores"]) == {"human", "ai"}
assert abs(sum(row["scores"].values()) - 1.0) < 0.01
assert row["label"] in {"human", "ai"}
@pytest.mark.integration
def test_desklib_loads_via_custom_class_and_scores(capsys):
from app.model import DetectorModel
from app.model_registry import get_model_config
m = DetectorModel(get_model_config("desklib-ai-detector"))
m.load()
out = m.predict([AI_TEXT, HUMAN_TEXT])
for row in out:
_assert_detector_output(row)
with capsys.disabled():
print(f"\n[desklib] P(ai) ai_text={out[0]['scores']['ai']} "
f"human_text={out[1]['scores']['ai']}")
@pytest.mark.integration
@pytest.mark.parametrize("model_id", ["fakespot-ai-detector", "oxidane-ai-detector"])
def test_softmax_detectors_canonical_labels(model_id, capsys):
from app.model import DetectorModel
from app.model_registry import get_model_config
m = DetectorModel(get_model_config(model_id))
m.load()
# Canonical labels were derived from the checkpoint's own id2label.
assert set(m._softmax_labels) == {"human", "ai"}
out = m.predict([AI_TEXT, HUMAN_TEXT])
for row in out:
_assert_detector_output(row)
with capsys.disabled():
print(f"\n[{model_id}] P(ai) ai_text={out[0]['scores']['ai']} "
f"human_text={out[1]['scores']['ai']}")
|