Skip to main content
 █████╗ ██╗      ██████╗ ███████╗████████╗███████╗ ██████╗████████╗
██╔══██╗██║      ██╔══██╗██╔════╝╚══██╔══╝██╔════╝██╔════╝╚══██╔══╝
███████║██║█████╗██║  ██║█████╗     ██║   █████╗  ██║        ██║
██╔══██║██║╚════╝██║  ██║██╔══╝     ██║   ██╔══╝  ██║        ██║
██║  ██║██║      ██████╔╝███████╗   ██║   ███████╗╚██████╗   ██║
╚═╝  ╚═╝╚═╝      ╚═════╝ ╚══════╝   ╚═╝   ╚══════╝ ╚═════╝   ╚═╝

SCORE YOUR OWN PROSE // BEFORE A TEACHER SCORES IT FOR YOU

a local, offline AI-writing detector and IB word counter for drafts you actually wrote

pypi language runs model license telemetry


🔍 What is this

A command-line tool that reads a .docx or .txt and scores each paragraph 0–1 on how AI-generated it reads, using the desklib/ai-text-detector-v1.01 DeBERTa model — the one sitting at #1 on the RAID benchmark. Everything runs on your own machine; after the first model download it never touches the network. You point it at your Extended Essay, it tells you which paragraphs sound like a language model wrote them.

The point isn't to cheat a detector. It's the opposite: I write my own drafts, and sometimes my own honest prose still trips these classifiers because that's what earnest formal writing looks like to them. This flags those paragraphs so I can reword before a teacher runs Turnitin and has an awkward conversation with me about it.

It is directional, not oracular. A high score means "reword this," not "you're caught." It is not, and cannot be, the number Turnitin shows a teacher.

nick@aidetect:~$ aidetect score EE-clean.txt
P  1   0.08  [##------------------]
P  2   0.71  [##############------]  <-- AI-ish
average AI score: 0.34   |   1/6 paragraphs flagged
reminder: directional only, not a Turnitin score.

🧠 The detection engine

feature what it actually does
01 per-paragraph scoring what it actually catches — splits your draft and scores each paragraph, so you fix the two bad ones instead of rewriting everything
02 docx + txt input reads Word files straight (paragraphs, no headings) or plain text split on blank lines
03 prose extractor aidetect extract strips headings, bullets, footnotes and your own note-scaffolding first, so the score is about writing, not structure
04 offline after setup first run pulls ~1.5GB of model, every run after is airgapped — your essay never leaves the laptop
05 second opinion cross-check against the lighter Ejhfast/fast-ai-detector when one model's paranoia isn't enough
06 Binoculars (Gemma 4) a training-free perplexity-ratio detector — near chance with small Qwen pairs, but 96% on the labelled set once swapped to a Gemma 4 pair; see below
07 IB word count aidetect count (--json for scripts and agents) counts what the IB counts — no headings, quotes, tables, footnotes, citations or bibliography — and splits the total by section, so an over-long draft tells you where

🚀 Run it

uv tool install aidetect      # or: pipx install aidetect
aidetect                                   # list the five subcommands
aidetect count "draft.docx" --limit 4000   # IB word count, by section
aidetect count "draft.docx" --json         # same, as one JSON object
aidetect score "draft.docx"                # score a whole draft
aidetect score --text "one sentence"       # score a single string
aidetect bino  "draft.docx" --mlx --pair gemma

count and extract are instant and need no model. score and bino need a machine that can hold a transformer: built and tested on an 18GB Apple Silicon Mac, MPS-accelerated. Their first run downloads the model and will sit there for a minute — that's normal, not a hang. Every run after is fast and offline.

On Apple Silicon the Gemma 4 MLX pair installs automatically. Elsewhere it is skipped and the Qwen pairs still work.

--json

count takes --json and prints exactly one object on stdout, nothing else:

{"sections": [{"title": "Introduction", "words": 812}], "total": 3940, "limit": 4000, "over": -60}

Every key is always present. limit and over are null when no --limit was given — that means "does not apply", not "could not be read". A draft with no prose is an empty sections list and exit 0. Errors go to stderr with a non-zero exit, so consumers branch on the exit code rather than parsing error text.

🔩 Under the hood

flowchart LR
    A[.docx / .txt] --> B[aidetect extract<br/>strip non-prose]
    B --> C[read_paragraphs<br/>>= 25 words]
    C --> D[desklib DeBERTa<br/>mean-pool + sigmoid]
    D --> E[per-paragraph<br/>0-1 score + flags]
file job
src/aidetect/cli.py the aidetect entry point — dispatches subcommands, importing each lazily so count never loads torch
src/aidetect/text.py shared, torch-free: what counts as prose, what ends a document, how a .docx is read
src/aidetect/count.py the IB word count — sections, citation stripping, budget
src/aidetect/detect.py loads the desklib model, scores each paragraph, prints the bars and flags
src/aidetect/extract.py pulls clean prose out of a .docx into a .txt — drops headings, bullets, note-labels
src/aidetect/binoculars.py training-free perplexity-ratio scorer over a base+instruct LM pair (Qwen, or Gemma 4 via --mlx; see below)
src/aidetect/calibrate.py fits a threshold on a labelled set you supply, saves it to ~/.config/aidetect
src/aidetect/paths.py where thresholds are looked up — ~/.config/aidetect first, then the ones in the package
src/aidetect/thresholds/ the thresholds shipped with the package; a threshold you fit yourself wins over these
corpora/ my labelled calibration sets. Repo-only, deliberately not shipped in the package
tests/ count rules and Binoculars math, both self-checking, no model download
pyproject.toml package metadata and dependencies — torch · transformers · python-docx, plus mlx-vlm on Apple Silicon

🔭 Binoculars: shelved, then revived by Gemma 4

Binoculars is a training-free detector: run text through two LMs that share a tokenizer (a base "observer" and an instruct "performer") and divide perplexity by cross-perplexity. Its designed model pair is Falcon-7B ×2 (~28GB) — too big for an 18GB Mac, so the fallback was a small same-family pair (Qwen2.5-0.5B or 1.5B) that fits.

aidetect calibrate scores a labelled set — mine is 12 real pre-2020 IB Extended Essay paragraphs vs 12 LLM-written ones on the same topics — and finds the best separating threshold. Measured across pairs:

pair best separation chance
Qwen2.5-0.5B 62% 50%
Qwen2.5-1.5B 67% 50%
Gemma 4 E2B 96% 50%

The Qwen pairs sit near a coin flip: their human and AI score clusters almost completely overlap, because the perplexity gap Binoculars exploits is sharp in larger models and mush in sub-2B ones. That was the original negative result. Swapping in a Gemma 4 pair opens a clean gap (human mean 0.90 vs AI 0.71) and separates the set at 96%. Gemma 4 ships as a multimodal checkpoint, so --mlx quantizes it to 4-bit and runs it text-only through mlx-vlm, fitting the 18GB Mac in ~6GB:

aidetect bino IA-clean.txt --mlx --pair gemma   # uses the shipped threshold

# refit the threshold on your own labelled set
aidetect calibrate --human-dir corpora/human --ai-dir corpora/ai --mlx --pair gemma

desklib stays the primary detector; Binoculars is now a usable second opinion rather than a dead end. The calibration sets are not shipped with the package — clone the repo to reproduce the numbers, or point --human-dir/--ai-dir at your own. Your fitted threshold lands in ~/.config/aidetect and takes precedence over the shipped one, so it survives an upgrade.

🧪 The peer set: genre context, not a human class

The 12 human samples are Extended Essays: English, History, Biology, Physics, Philosophy. A CS IA is a different animal, all database schemas, GUI components and method-by-method justification, and technical prose is inherently more predictable token-by-token, which drags perplexity-ratio scores down no matter who typed it. So a CS IA scoring below the EE human mean means less than it looks.

corpora/peer/ holds 12 paragraphs of real IB Computer Science IA prose (5 projects, 5 authors: sudokuMaster, IBOrganizer, MyCalendar, and two IBO-published new-syllabus specimens). Measured against the same anchors:

set Binoculars mean desklib mean
human (2008 EEs, verified pre-2020) 0.90 n/a
peer (CS IAs, 2021–2025) 0.85 0.46
ai (LLM-written, matched topics) 0.71 n/a

The genre gap is real and it is about 0.05 on Binoculars. Score your IA against peer, not against human.

It is not a human class and it never fits a threshold. Every source postdates ChatGPT; three of the five were written in 2025. None carries an authorship attestation, and in 2025 a fair share of student IAs were not written unaided. Fold that into human/ and any AI-assisted sample drags the mean down, lowers the threshold, and the tool starts clearing drafts for the wrong reason, a detector that reassures instead of measures. aidetect calibrate reads only the two folders you name, so peer/ stays out of threshold fitting by construction, not by discipline.

What it can tell you: "my prose scores like other IAs in this genre." What it can never tell you: "my prose is human." Matching a set you cannot vouch for proves you are not an outlier, nothing more.

Stack: python · pytorch · transformers · mlx-vlm · desklib DeBERTa

The lighter cross-check tool lives at Ejhfast/fast-ai-detector — it's a separate repo, not vendored here.


Nick Trimandylis

I WRITE MY OWN ESSAYS — THIS JUST CHECKS THEY STILL READ LIKE IT

MIT licensed — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aidetect-0.1.0.tar.gz (24.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aidetect-0.1.0-py3-none-any.whl (22.9 kB view details)

Uploaded Python 3

File details

Details for the file aidetect-0.1.0.tar.gz.

File metadata

  • Download URL: aidetect-0.1.0.tar.gz
  • Upload date:
  • Size: 24.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for aidetect-0.1.0.tar.gz
Algorithm Hash digest
SHA256 07153c025248bb88301efadcb712f2b1f3957a68bcda906d3026a26a6bcd0568
MD5 ef800b7e37b7b3c8b5381daded098e39
BLAKE2b-256 e6aaac12b0f5636e128fda8de1f62e531dfa01ca52e05a07c3e710dc6b84616b

See more details on using hashes here.

Provenance

The following attestation bundles were made for aidetect-0.1.0.tar.gz:

Publisher: publish.yml on nitrimandylis/aidetect

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aidetect-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: aidetect-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 22.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for aidetect-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f0f77531dcb7605920a31a3347a47060a4b2836c2c38663608aa36d82082b032
MD5 767737ef1c3a4aa875c8081a665e484f
BLAKE2b-256 1a191676ad081c368c976006af1f939c5bc0ce5c90794f0aa7ab3ec85755ffdf

See more details on using hashes here.

Provenance

The following attestation bundles were made for aidetect-0.1.0-py3-none-any.whl:

Publisher: publish.yml on nitrimandylis/aidetect

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page