Skip to main content

detectorproof

Measure the detectors before you trust any of them.

detectorproof is not a synthetic-speech detector and must not become one. It is an instrument for measuring detectors: what a declared panel of frozen checkpoints can and cannot do on your material, and what their judgments are reacting to.

One command is built: panel. It scores a declared set of frozen detectors over a labelled control set, reports the area under the ROC curve for each, and refuses to let an incompetent one contribute to anything downstream.

No audio is read and no model is loaded. It works on a score table you already have, has no dependencies, and runs on a machine that will never hold the corpus.

pip install detectorproof

Or from a clone, which is what you want if you intend to run the tests:

git clone https://github.com/Mormolykos/detectorproof.git
cd detectorproof
pip install .          # or: pip install -e ".[dev]" && pytest

Python 3.10 or newer, no runtime dependencies. Both forms install the detectorproof command and the importable package. See CHANGELOG.md for release status.

Why this exists

On one corpus, three widely benchmarked anti-spoofing detectors sat at chance while their model cards advertised sub-1% equal error rates. A panel that quietly averaged them in would have reported a confident number built partly on models that could not do the task at all.

Separately, and this is the finding the package is built around: genuine human speech can move strongly toward "synthetic" after ordinary benign processing that generates nothing. A neural-codec round trip of real studio recordings moved 9 of 13 frozen detectors past multiple-comparison correction, three of them on 47 of 47 utterances at the maximum a rank statistic can reach. A phase-retrieval algorithm from 1984 moved 12 of 13.

Those numbers are not asserted here. They come from a published, independently audited study, and every one of them regenerates from its deposit:

Gkilis, P. (2026). Reconstruction history, not synthesis: benign processing moves frozen detectors on genuine human speech. Zenodo. 10.5281/zenodo.22819223 · CC BY 4.0

⚠️ Read its scope limits before quoting it: three speakers, 47 utterances, neutral speech, one language; seven of the thirteen detectors come from a single research group; no mechanism is identified and no listening test was run. Thirteen of that paper's own claims were withdrawn during three rounds of independent adversarial audit, and the withdrawals are published in it.

So a detector score is not interpretable without knowing the provenance of the audio it was computed on — and before any of that, you need to know which detectors on your panel can do the task on your material at all. That is what panel answers.

Usage

detectorproof panel \
  --panel panel.json \
  --scores scores.csv \
  --labels labels.csv \
  --condition CTRL

panel.json — the declaration, fixed before the run.

[
  {"name": "det-a", "orientation": "higher_bonafide", "identity": "sha256:1f3a...", "source": "..."},
  {"name": "det-b", "orientation": "higher_synthetic", "identity": "rev:c1dbe2ae"},
  {"name": "det-c", "orientation": "unresolved", "identity": "commit:72c32e28"}
]

orientation is resolved from the model's own documentation or training-label path, never from what makes your benchmark look right. unresolved is a first-class value: the detector is scored and reported, and its sign is not interpreted.

identity pins the weights — a revision, a commit, a file hash. It is required. A panel that cannot say which checkpoint produced a number is not frozen.

scores.csv — detector,item,condition,score. One row per detector per item per condition. Scores are in each detector's native units. A missing measurement is written NOT_MEASURED (or n/a), never 0.

labels.csv — item,label, where label is bonafide or synthetic.

In Python:

from detectorproof import Detector, run_panel, render

report = run_panel(detectors, scores, labels, condition="CTRL")
print(render(report))

for result in report.competent_detectors():   # the only set you may aggregate over
    ...

The refusals

These are enforced in code, not described here. Each was bought by a specific measured failure.

refusal the failure that bought it
A detector at or near chance is NOT COMPETENT ON THIS MATERIAL — reported in full, excluded from every aggregate three benchmarked detectors at chance while advertising sub-1% EER
The panel is declared before the run. An undeclared detector in the scores is a fault; a declared detector with no scores stays in the report as NOT MEASURED a panel assembled after seeing results is a selection, not a measurement
Two scores for one detector and item are a fault, not something to average overlapping analysis windows treated as independent samples inflated one published proportion from 75% to 86%
NOT MEASURED is a third state — never zero, never an absence of effect a missing measurement reported as zero is worse than reporting nothing
A near miss at the band edge is recorded, not resolved by widening the band one detector landed at 0.447 against a declared band of [0.45, 0.55] and the band stayed where it was

There is deliberately no function that recommends a transformation to move a score, and there will not be one. In the work this came from, an intervention tuned against a chosen detector statistic made the audio more separable, not less — one detector's AUC moved from 0.576 to 0.763. The instrument reports; it does not advise. That refusal is the point, not an omission.

What a verdict means

NOT COMPETENT ON THIS MATERIAL is a statement about that checkpoint on your control set. It is never a statement about a detector's general quality. Every detector was trained by someone else, on other data, for other purposes.

An empty competent set is a reportable result about your panel and your material. It is not evidence that your audio is genuine, and it is not evidence that detection is impossible. Absence of evidence is not evidence of absence, and nothing downstream may run on an empty competent set.

Design

Zero dependencies in the core. Ties are handled by midranks everywhere, because a detector whose scores compress near zero produces long runs of equal values and a rank statistic that breaks ties arbitrarily reports a different answer depending on the order its input arrived in. AUC is computed from the rank sum rather than by counting pairs.

Every field this package reads has a declared domain — its type, its admissible values, and what absence means. Validation asks whether a value is inside that declaration; it does not carry a list of bad values anyone has seen. That list is unbounded and the domain is not.

The family

trainproof (training runs) · ttsproof (TTS failure modes) · spkproof (speaker-verification false rejection) · detectorproof (detector competence and robustness).

"Did the processing preserve speaker identity?" is a different question with different statistics, and spkproof answers it. Version 0.1.0 has no dependency on spkproof and does not call it — the two are used side by side, not chained.

Status

panel is built and tested. probe, identity and falsepositive are specified and not built. They are deliberately not built at the same time — panel is the gate the other three depend on, and it has to refuse correctly on a detector already known to be incompetent before anything is built on top of it.

Licence

MIT.

Release files for detectorproof 0.1.0

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

Source distribution (sdist)

Source distribution for detectorproof 0.1.0
File Size Uploaded
detectorproof-0.1.0.tar.gz 51.0 kB Details

Built distribution (wheel)

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

Total release size: 75.4 kB

Release files / detectorproof-0.1.0.tar.gz

Download URL detectorproof-0.1.0.tar.gz
Size 51.0 kB
Tags Source
SHA-256 checksum
How to use checksums
3a3b2d2f86c511a016380a32567b7e5fd597044affce3292c60fa5700df85efa
BLAKE2b-256 checksum
How to use checksums
c06e01f810d82220637c9a9e32b01e2f4834f90ace2dad70714b1474217398c3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / detectorproof-0.1.0-py3-none-any.whl

Download URL detectorproof-0.1.0-py3-none-any.whl
Size 24.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
decde7c28dfddcc9b421891032f39a49315f3aff10ebbb1b8224e1eedfa7e40c
BLAKE2b-256 checksum
How to use checksums
4d5747fc7279172e762a6afd513e46d1a6a3de75a53c08a1bf619a13a11abeda
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

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