Skip to main content

ductus

Gauge which parts of a text read as machine-written, and say why — with every finding anchored to the exact characters that carry it.

from ductus import gauge

report = gauge(open("draft.md").read())
report.document.label  # 'mixed-signals'
report.segments[3].lean  # +0.62  (-1 human … +1 machine)
report.segments[3].signals[0].note
# "a colon introducing a three-part parallel enumeration -- a textbook assistant construction"
pip install ductus
ductus gauge draft.md                                  # a readable diagnosis
ductus gauge draft.md --format html --out report.html  # shaded, hover for the reason
ductus gauge draft.md --format json                    # for a program

In palaeography the ductus is the characteristic manner and sequence of strokes by which a scribe's hand is recognised. This package looks for the equivalent in prose.

There is no percentage in this package, and that is the point

Every detector on the market emits a number like "87% AI". That number reads as a calibrated probability, is not one, and is how people get falsely accused. What you get here instead is three things you can argue with: a lean in [-1, +1], an evidence strength, and a coarse label. Behind each of them is a list of signals, each with a direction, a weight, the detector that produced it, a plain-language reason, and the exact span it came from.

Four things are true and belong in any report you make from this:

  • Heavily-edited human writing and model-assisted writing look the same. Re-reading a hard message ten times sands off exactly the irregularity that marks it as human.
  • Detectors over-flag non-native English writers — 61.3% false positives across seven commercial detectors on non-native TOEFL essays, against near-zero on native-speaker controls [1].
  • Register contamination is real: people who read model output all day start writing like it, unassisted.
  • No detector survives a motivated adversary, and that is a proven result rather than a gap in current tooling [2].

So this package describes text. It does not make claims about people, and it should not be used to.

What it looks at

Four detectors ship, all deterministic, all free, none needing a model or a key.

Detector Finds Example
tells Catalogue phrases models overuse, tiered by confidence "delve", "it's important to note", "In conclusion,"
forensic Mechanical traces of how the text was produced a hard line break mid-sentence, mixed straight-and-curly apostrophes, trailing whitespace, em-dash density
rhetoric Sentence shapes a phrase list cannot see "not X but Y", a colon introducing a three-part parallel enumeration, concession-then-pivot
rhythm Burstiness — how much sentence length varies metronomic paragraphs

Note that several of these argue for a human. A detector that can only ever accuse is not a measuring instrument. In practice the mechanical signals are often the most decisive thing in a file, in either direction.

The deterministic pass finds phrases, artifacts and a few shapes. It cannot find prose that is machine-written and bland — for that a model has to read it, which is what the shipped agent skills are for.

Agent skills

The primary surface. Two skills and a subagent ship inside the package and install with one command:

ductus install-skills --write          # links them into ~/.claude/skills
  • ductus — what may and may not be claimed from a text, and which task is which.
  • ductus-gauge — the full reading: the deterministic scan, then a judgment pass with rubrics for the nine machine-leaning and six human-leaning shapes that regular expressions miss, then the write-up.
  • ductus-reader (subagent) — does the whole reading in its own context and returns a finished diagnosis.

An agent's own reading folds back in beside the deterministic signals, anchored by quote:

ductus gauge draft.md --judgments judgments.json --format html --out report.html

A quote that no longer occurs is dropped rather than mis-anchored, so re-running after an edit is safe.

Seams

Three, each one keyword argument, each defaulting to something that genuinely works:

gauge(text, segmenter="sentence")  # or "paragraph", "document", or a callable
gauge(
    text, detectors=["forensic", "rhetoric"]
)  # or your own (text, span) -> Iterator[Signal]
gauge(text, aggregate=my_calibrated_scorer)  # replace the scoring model wholesale

A detector is a plain function (text, span) -> Iterator[Signal]. There is no base class and nothing to register. Adding Fast-DetectGPT, Binoculars or a vendor API means writing one more function of that shape — see the roadmap.

For long documents, iter_segments is the streaming core and gauge is the batch facade over it.

Spans survive editing

Every Span carries character offsets and the W3C Web Annotation redundant selectors (quote, prefix, suffix), because plain offsets do not survive an edit to the text. That is what lets a viewer re-find a finding after the document changed, and what a future edit-and-re-score UI is built on.

Relationship to deslop

ductus is the read side: where does this text read as machine-written, and why. acquaint's deslop is the write side: make my draft not read that way, in my voice, calibrated to this reader. They share this package's tells catalogue — acquaint imports it — and compose naturally: gauge, deslop the flagged spans, gauge again.

They are separate packages because they have different inputs. deslop needs a model of the reader; ductus must work on a stranger's text with nothing but the text.

Install

pip install ductus              # the core: pyyaml and cw, nothing else
pip install "ductus[local]"     # + model-based detectors, offline, no API key
pip install "ductus[api]"       # + vendor detector adapters

References

[1] Liang W, Yuksekgonul M, Mao Y, Wu E, Zou J. GPT detectors are biased against non-native English writers. Patterns 2023.

[2] Sadasivan VS, Kumar A, Balasubramanian S, Wang W, Feizi S. Can AI-Generated Text be Reliably Detected?

[3] Sanderson R, Ciccarese P, Young B. Web Annotation Data Model. W3C Recommendation, 2017.

[4] Tolstykh I, et al. LLMTrace: A Corpus for Classification and Fine-Grained Localization of AI-Written Text. arXiv:2509.21269. — the ground-truth fixture in tests/fixtures/ is a slice of this, Apache-2.0.

Metadata

Release files for ductus 0.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ductus 0.0.3
File Size Uploaded
ductus-0.0.3.tar.gz 42.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ductus 0.0.3
File Interpreter ABI Platform
ductus-0.0.3-py3-none-any.whl Python 3 none any Details

Total release size: 82.0 kB

Release files / ductus-0.0.3.tar.gz

Download URL ductus-0.0.3.tar.gz
Size 42.7 kB
Tags Source
SHA-256 checksum
How to use checksums
34ed345e5eafa28622f0cec0937b3434196357b08890734c12390075c081ca45
BLAKE2b-256 checksum
How to use checksums
d115f060acd37d9bd4cbed89a0e27a6c72a45979ea831032b7871169e4f02dd0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / ductus-0.0.3-py3-none-any.whl

Download URL ductus-0.0.3-py3-none-any.whl
Size 39.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e8a5d7f5e594f14865bff28dd01c71c337faf8402a6fc817c80b40ab6addb24c
BLAKE2b-256 checksum
How to use checksums
f90e8491810721eb415ab521cabce676cfb73be9ef5e09702a796dbb8c13738a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.0.14

2 release files

0.0.13

2 release files

0.0.12

2 release files

0.0.11

2 release files

0.0.10

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

This release

0.0.3 This release

2 release files

0.0.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page