detectorproof 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ name: CI
2
+
3
+ # `workflow_call` matters: release.yml invokes this file rather than repeating
4
+ # its steps, so there is exactly one definition of "the gate" and a release can
5
+ # never run a weaker one than a pull request.
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ pull_request:
10
+ workflow_call:
11
+
12
+ # Read-only for every job in this file. Nothing here needs to write to the
13
+ # repository, and nothing here may publish: `id-token` is never granted, so no
14
+ # job defined in this workflow can mint a PyPI credential even if a step tried.
15
+ permissions:
16
+ contents: read
17
+
18
+ concurrency:
19
+ group: ci-${{ github.ref }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ attribution:
24
+ name: attribution
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ # v7.0.1
28
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
29
+ with:
30
+ # The scan reads recent commit messages; the default shallow clone of
31
+ # depth 1 would silently scan one commit and pass.
32
+ fetch-depth: 25
33
+ # v7.0.0
34
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
35
+ with:
36
+ python-version: "3.12"
37
+ - name: Scan the tracked tree, the metadata and recent commits
38
+ run: python scripts/ci.py attribution
39
+
40
+ lint:
41
+ name: ruff
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ # v7.0.1
45
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
46
+ # v7.0.0
47
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
48
+ with:
49
+ python-version: "3.12"
50
+ # Pinned on purpose. An unpinned linter turns a green repository red on a
51
+ # morning when nobody changed anything. Bumping this is a pull request.
52
+ - name: Install ruff
53
+ run: pip install "ruff==0.16.1"
54
+ - name: Lint
55
+ run: ruff check .
56
+
57
+ test:
58
+ name: pytest
59
+ runs-on: ${{ matrix.os }}
60
+ strategy:
61
+ # One red cell must not hide the other seven. Which versions fail is the
62
+ # information a matrix exists to produce.
63
+ fail-fast: false
64
+ matrix:
65
+ os: [ubuntu-latest, windows-latest]
66
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
67
+ steps:
68
+ # v7.0.1
69
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
70
+ # v7.0.0
71
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
72
+ with:
73
+ python-version: ${{ matrix.python-version }}
74
+ - name: Install the package and its dev extra
75
+ run: pip install -e ".[dev]"
76
+ - name: Run the test suite
77
+ run: pytest -q
78
+ # This package has no optional dependencies, so nothing can legitimately
79
+ # skip. A skip means a test that runs in no environment at all.
80
+ - name: Nothing is skipped
81
+ run: python scripts/ci.py noskips
82
+
83
+ clone:
84
+ name: a fresh clone can run its own tests
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ # v7.0.1
88
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
89
+ # v7.0.0
90
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
91
+ with:
92
+ python-version: "3.12"
93
+ # This is a src-layout package. Every job above installs it first and so
94
+ # would never notice if a bare checkout could not collect its own tests -
95
+ # the exact defect this job exists to catch in the sibling repository.
96
+ - name: Install pytest and nothing else
97
+ run: pip install "pytest>=8"
98
+ - name: Run the suite against the bare checkout
99
+ run: pytest -q
100
+
101
+ contract:
102
+ name: published exit codes
103
+ runs-on: ubuntu-latest
104
+ steps:
105
+ # v7.0.1
106
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
107
+ # v7.0.0
108
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
109
+ with:
110
+ python-version: "3.12"
111
+ - name: Install the package
112
+ run: pip install -e "."
113
+ # The README publishes what each exit code means to anyone wiring this
114
+ # into a pipeline. Unit tests cover the functions; this covers the
115
+ # promise, through the console script.
116
+ - name: Exit-code contract
117
+ run: python scripts/ci.py contract
118
+
119
+ package:
120
+ name: build and verify the artifact
121
+ runs-on: ubuntu-latest
122
+ steps:
123
+ # v7.0.1
124
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
125
+ # v7.0.0
126
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
127
+ with:
128
+ python-version: "3.12"
129
+ - name: Install build tooling
130
+ run: pip install "build==1.5.1" "twine==6.2.0"
131
+ - name: Versions agree
132
+ run: python scripts/ci.py version
133
+ - name: Build sdist and wheel
134
+ run: python -m build
135
+ # The filenames carry the version. A build that quietly produced a stale
136
+ # one would still pass `twine check` and still install, so the names are
137
+ # checked against the project version before anything downstream trusts
138
+ # them.
139
+ - name: Distributions match the project version
140
+ run: python scripts/ci.py artifact
141
+ - name: Metadata is renderable
142
+ run: twine check dist/*
143
+ # Everything above tests the source tree. This installs the artifact a
144
+ # user would download, into an interpreter with no source directory on its
145
+ # path, and asks it to work.
146
+ - name: Install the wheel into a clean environment and smoke it
147
+ run: python scripts/ci.py wheelcheck
148
+ # v7.0.1
149
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
150
+ with:
151
+ name: distributions
152
+ path: dist/
@@ -0,0 +1,98 @@
1
+ name: Release
2
+
3
+ # A tag is the only trigger. There is no manual "publish" button, no
4
+ # workflow_dispatch, and no branch that publishes on merge, so the released
5
+ # version is always a named point in history that someone can check out.
6
+ #
7
+ # Read this line before typing `git tag`: pushing a v* tag IS publishing to
8
+ # PyPI. There is no second confirmation step anywhere below.
9
+ on:
10
+ push:
11
+ tags: ["v*"]
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ # The same file a pull request runs - not a copy of it and not a subset. A
18
+ # release that could pass a weaker gate than a pull request is the fault this
19
+ # reuse exists to prevent. ci.yml already builds the distributions, checks
20
+ # their filenames carry the project version, renders the metadata and installs
21
+ # the wheel into a clean interpreter, so there is nothing left for this file
22
+ # to verify about the artifact itself.
23
+ gate:
24
+ uses: ./.github/workflows/ci.yml
25
+
26
+ publish:
27
+ name: publish to PyPI
28
+ needs: gate
29
+ runs-on: ubuntu-latest
30
+ environment:
31
+ name: pypi
32
+ url: https://pypi.org/p/detectorproof
33
+ permissions:
34
+ # A job-level `permissions:` block REPLACES the workflow-level one, it
35
+ # does not add to it. Declaring only `id-token: write` here therefore set
36
+ # `contents` to none, and `actions/checkout` needs `contents: read` - on a
37
+ # private repository the checkout below would have failed. Both are listed
38
+ # explicitly.
39
+ #
40
+ # `contents` stays READ. Creating the GitHub release needs write, and that
41
+ # is a separate job with its own block.
42
+ #
43
+ # Trusted publishing: PyPI mints a short-lived token from this OIDC
44
+ # identity, so there is no long-lived API token in the repository, in an
45
+ # organisation secret, or on the maintainer's laptop. Nothing to leak and
46
+ # nothing to rotate.
47
+ contents: read
48
+ id-token: write
49
+ steps:
50
+ # v7.0.1
51
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
52
+ # v7.0.0
53
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
54
+ with:
55
+ python-version: "3.12"
56
+
57
+ # `ci.py version` already checks pyproject against the package's
58
+ # __version__. This checks both against the tag, which is the one pairing
59
+ # no local gate can see - a correct package under a wrong name in history.
60
+ - name: The tag matches the version in the source
61
+ run: python scripts/ci.py version --expect "${{ github.ref_name }}"
62
+
63
+ # Publish the artifact the gate verified. Rebuilding here would upload
64
+ # bytes nothing tested: a different wheel, from the same source, built by
65
+ # a job holding an OIDC token.
66
+ # v7.0.0
67
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
68
+ with:
69
+ name: distributions
70
+ path: dist
71
+
72
+ # Every third-party action in this repository is pinned to a commit SHA,
73
+ # not a tag, with the human-readable version in the comment above it. A
74
+ # moving tag is a moving credential holder, and SHA-pinning only the
75
+ # publisher would leave the other actions in this same OIDC-enabled job
76
+ # able to change under it.
77
+ # v1.14.2
78
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
79
+
80
+ github-release:
81
+ name: GitHub release
82
+ needs: publish
83
+ runs-on: ubuntu-latest
84
+ permissions:
85
+ contents: write
86
+ steps:
87
+ # v7.0.1
88
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
89
+ # v7.0.0
90
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
91
+ with:
92
+ name: distributions
93
+ path: dist
94
+ # v2.6.2
95
+ - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
96
+ with:
97
+ files: dist/*
98
+ generate_release_notes: true
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-09-22
4
+
5
+ First release.
6
+
7
+ `panel` only. The instrument's own build order puts `panel` first and says not to build all
8
+ four commands at once: it is the gate `probe`, `identity` and `falsepositive` depend on, and
9
+ it has to refuse correctly on a detector already known to be incompetent before anything is
10
+ built on top of it.
11
+
12
+ **Added**
13
+
14
+ - `run_panel` — score a declared panel of frozen detectors over a labelled control set and
15
+ return a per-detector competence verdict.
16
+ - Five refusals enforced in code: chance-level exclusion, panel declared before the run,
17
+ duplicate detector/item rows rejected rather than averaged, NOT MEASURED as a third state,
18
+ band-edge results recorded rather than re-banded.
19
+ - `domain` — a declared domain for every field read, with rejection messages that name the
20
+ domain rather than a catalogue of values previously seen to be wrong.
21
+ - `stats` — tie-aware midranks, AUC from the rank sum, Holm–Bonferroni with the running
22
+ maximum.
23
+ - `report.render` — plain-text report in which excluded detectors still appear in full and a
24
+ null is worded as no evidence of a difference.
25
+ - `detectorproof panel` command line reading three files a study already has.
26
+ - Property fuzz over generated junk values, asserting invariants rather than outcomes.
27
+
28
+ **Deliberately absent**
29
+
30
+ - Any function that recommends a transformation to move a detector score. Tuning audio
31
+ against a chosen detector statistic was measured, in the work behind this package, to make
32
+ the audio more separable rather than less.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Panagiotis (Panos) Gkilis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,15 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ recursive-include tests *.py
5
+
6
+ # The sdist ships its tests, so it must ship what those tests read. The audit
7
+ # regression suite exercises the release gate itself and the workflow
8
+ # configuration, so an sdist carrying the tests but not these files produced ten
9
+ # failures when its own bundled suite was run from the unpacked archive.
10
+ #
11
+ # Nothing here is private: the gate script and the two workflow files are the
12
+ # same ones a reader gets from the repository.
13
+ include scripts/ci.py
14
+ include .github/workflows/ci.yml
15
+ include .github/workflows/release.yml
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: detectorproof
3
+ Version: 0.1.0
4
+ Summary: Measure the detectors before trusting them: a declared panel of frozen synthetic-speech detectors, scored on your control set, with a chance-level detector refused rather than averaged in.
5
+ Author-email: "Panagiotis (Panos) Gkilis" <bedvibe@bedvibe.studio>
6
+ License: MIT
7
+ Project-URL: Homepage, https://ai.bedvibe.studio/
8
+ Keywords: audio-deepfake-detection,synthetic-speech-detection,detector-robustness,false-positives,neural-audio-codec,analysis-resynthesis,pre-registration,reproducibility,auc,multiple-comparisons
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Topic :: Scientific/Engineering
11
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Development Status :: 3 - Alpha
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest; extra == "dev"
23
+ Requires-Dist: ruff>=0.16; extra == "dev"
24
+ Requires-Dist: mypy>=2.3; extra == "dev"
25
+ Requires-Dist: build>=1.2; extra == "dev"
26
+ Requires-Dist: twine>=6; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # detectorproof
30
+
31
+ **Measure the detectors before you trust any of them.**
32
+
33
+ `detectorproof` is not a synthetic-speech detector and must not become one. It is an
34
+ instrument for measuring detectors: what a declared panel of frozen checkpoints can and
35
+ cannot do on *your* material, and what their judgments are reacting to.
36
+
37
+ One command is built: `panel`. It scores a declared set of frozen detectors over a labelled
38
+ control set, reports the area under the ROC curve for each, and refuses to let an
39
+ incompetent one contribute to anything downstream.
40
+
41
+ No audio is read and no model is loaded. It works on a score table you already have, has no
42
+ dependencies, and runs on a machine that will never hold the corpus.
43
+
44
+ ```
45
+ pip install detectorproof
46
+ ```
47
+
48
+ Or from a clone, which is what you want if you intend to run the tests:
49
+
50
+ ```
51
+ git clone https://github.com/Mormolykos/detectorproof.git
52
+ cd detectorproof
53
+ pip install . # or: pip install -e ".[dev]" && pytest
54
+ ```
55
+
56
+ Python 3.10 or newer, no runtime dependencies. Both forms install the `detectorproof`
57
+ command and the importable package. See `CHANGELOG.md` for release status.
58
+
59
+ ## Why this exists
60
+
61
+ On one corpus, three widely benchmarked anti-spoofing detectors sat at chance while their
62
+ model cards advertised sub-1% equal error rates. A panel that quietly averaged them in would
63
+ have reported a confident number built partly on models that could not do the task at all.
64
+
65
+ Separately, and this is the finding the package is built around: **genuine human speech can
66
+ move strongly toward "synthetic" after ordinary benign processing that generates nothing.**
67
+ A neural-codec round trip of real studio recordings moved 9 of 13 frozen detectors past
68
+ multiple-comparison correction, three of them on 47 of 47 utterances at the maximum a rank
69
+ statistic can reach. A phase-retrieval algorithm from 1984 moved 12 of 13.
70
+
71
+ Those numbers are not asserted here. They come from a published, independently audited
72
+ study, and every one of them regenerates from its deposit:
73
+
74
+ > Gkilis, P. (2026). *Reconstruction history, not synthesis: benign processing moves frozen
75
+ > detectors on genuine human speech.* Zenodo.
76
+ > [10.5281/zenodo.22819223](https://doi.org/10.5281/zenodo.22819223) · CC BY 4.0
77
+
78
+ ⚠️ Read its scope limits before quoting it: three speakers, 47 utterances, neutral speech,
79
+ one language; seven of the thirteen detectors come from a single research group; **no
80
+ mechanism is identified and no listening test was run.** Thirteen of that paper's own claims
81
+ were withdrawn during three rounds of independent adversarial audit, and the withdrawals are
82
+ published in it.
83
+
84
+ So a detector score is not interpretable without knowing the provenance of the audio it was
85
+ computed on — and before any of that, you need to know which detectors on your panel can do
86
+ the task on your material at all. That is what `panel` answers.
87
+
88
+ ## Usage
89
+
90
+ ```
91
+ detectorproof panel \
92
+ --panel panel.json \
93
+ --scores scores.csv \
94
+ --labels labels.csv \
95
+ --condition CTRL
96
+ ```
97
+
98
+ **`panel.json`** — the declaration, fixed before the run.
99
+
100
+ ```json
101
+ [
102
+ {"name": "det-a", "orientation": "higher_bonafide", "identity": "sha256:1f3a...", "source": "..."},
103
+ {"name": "det-b", "orientation": "higher_synthetic", "identity": "rev:c1dbe2ae"},
104
+ {"name": "det-c", "orientation": "unresolved", "identity": "commit:72c32e28"}
105
+ ]
106
+ ```
107
+
108
+ `orientation` is resolved from the model's own documentation or training-label path, never
109
+ from what makes your benchmark look right. `unresolved` is a first-class value: the detector
110
+ is scored and reported, and its *sign* is not interpreted.
111
+
112
+ `identity` pins the weights — a revision, a commit, a file hash. It is required. A panel that
113
+ cannot say which checkpoint produced a number is not frozen.
114
+
115
+ **`scores.csv`** — `detector,item,condition,score`. One row per detector per item per
116
+ condition. Scores are in each detector's native units. A missing measurement is written
117
+ `NOT_MEASURED` (or `n/a`), never `0`.
118
+
119
+ **`labels.csv`** — `item,label`, where label is `bonafide` or `synthetic`.
120
+
121
+ In Python:
122
+
123
+ ```python
124
+ from detectorproof import Detector, run_panel, render
125
+
126
+ report = run_panel(detectors, scores, labels, condition="CTRL")
127
+ print(render(report))
128
+
129
+ for result in report.competent_detectors(): # the only set you may aggregate over
130
+ ...
131
+ ```
132
+
133
+ ## The refusals
134
+
135
+ These are enforced in code, not described here. Each was bought by a specific measured
136
+ failure.
137
+
138
+ | refusal | the failure that bought it |
139
+ |---|---|
140
+ | 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 |
141
+ | 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 |
142
+ | 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% |
143
+ | **NOT MEASURED is a third state** — never zero, never an absence of effect | a missing measurement reported as zero is worse than reporting nothing |
144
+ | 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 |
145
+
146
+ There is deliberately **no function that recommends a transformation** to move a score, and
147
+ there will not be one. In the work this came from, an intervention tuned against a chosen
148
+ detector statistic made the audio *more* separable, not less — one detector's AUC moved from
149
+ 0.576 to 0.763. The instrument reports; it does not advise. That refusal is the point, not an
150
+ omission.
151
+
152
+ ## What a verdict means
153
+
154
+ `NOT COMPETENT ON THIS MATERIAL` is a statement about **that checkpoint on your control
155
+ set**. It is never a statement about a detector's general quality. Every detector was trained
156
+ by someone else, on other data, for other purposes.
157
+
158
+ An empty competent set is a reportable result about your panel and your material. It is
159
+ **not** evidence that your audio is genuine, and it is not evidence that detection is
160
+ impossible. Absence of evidence is not evidence of absence, and nothing downstream may run on
161
+ an empty competent set.
162
+
163
+ ## Design
164
+
165
+ Zero dependencies in the core. Ties are handled by midranks everywhere, because a detector
166
+ whose scores compress near zero produces long runs of equal values and a rank statistic that
167
+ breaks ties arbitrarily reports a different answer depending on the order its input arrived
168
+ in. AUC is computed from the rank sum rather than by counting pairs.
169
+
170
+ Every field this package reads has a **declared domain** — its type, its admissible values,
171
+ and what absence means. Validation asks whether a value is inside that declaration; it does
172
+ not carry a list of bad values anyone has seen. That list is unbounded and the domain is not.
173
+
174
+ ## The family
175
+
176
+ `trainproof` (training runs) · `ttsproof` (TTS failure modes) · `spkproof` (speaker-verification
177
+ false rejection) · **`detectorproof`** (detector competence and robustness).
178
+
179
+ "Did the processing preserve speaker identity?" is a different question with different
180
+ statistics, and `spkproof` answers it. **Version 0.1.0 has no dependency on `spkproof` and does
181
+ not call it** — the two are used side by side, not chained.
182
+
183
+ ## Status
184
+
185
+ `panel` is built and tested. `probe`, `identity` and `falsepositive` are specified and not
186
+ built. They are deliberately not built at the same time — `panel` is the gate the other three
187
+ depend on, and it has to refuse correctly on a detector already known to be incompetent
188
+ before anything is built on top of it.
189
+
190
+ ## Licence
191
+
192
+ MIT.
@@ -0,0 +1,164 @@
1
+ # detectorproof
2
+
3
+ **Measure the detectors before you trust any of them.**
4
+
5
+ `detectorproof` is not a synthetic-speech detector and must not become one. It is an
6
+ instrument for measuring detectors: what a declared panel of frozen checkpoints can and
7
+ cannot do on *your* material, and what their judgments are reacting to.
8
+
9
+ One command is built: `panel`. It scores a declared set of frozen detectors over a labelled
10
+ control set, reports the area under the ROC curve for each, and refuses to let an
11
+ incompetent one contribute to anything downstream.
12
+
13
+ No audio is read and no model is loaded. It works on a score table you already have, has no
14
+ dependencies, and runs on a machine that will never hold the corpus.
15
+
16
+ ```
17
+ pip install detectorproof
18
+ ```
19
+
20
+ Or from a clone, which is what you want if you intend to run the tests:
21
+
22
+ ```
23
+ git clone https://github.com/Mormolykos/detectorproof.git
24
+ cd detectorproof
25
+ pip install . # or: pip install -e ".[dev]" && pytest
26
+ ```
27
+
28
+ Python 3.10 or newer, no runtime dependencies. Both forms install the `detectorproof`
29
+ command and the importable package. See `CHANGELOG.md` for release status.
30
+
31
+ ## Why this exists
32
+
33
+ On one corpus, three widely benchmarked anti-spoofing detectors sat at chance while their
34
+ model cards advertised sub-1% equal error rates. A panel that quietly averaged them in would
35
+ have reported a confident number built partly on models that could not do the task at all.
36
+
37
+ Separately, and this is the finding the package is built around: **genuine human speech can
38
+ move strongly toward "synthetic" after ordinary benign processing that generates nothing.**
39
+ A neural-codec round trip of real studio recordings moved 9 of 13 frozen detectors past
40
+ multiple-comparison correction, three of them on 47 of 47 utterances at the maximum a rank
41
+ statistic can reach. A phase-retrieval algorithm from 1984 moved 12 of 13.
42
+
43
+ Those numbers are not asserted here. They come from a published, independently audited
44
+ study, and every one of them regenerates from its deposit:
45
+
46
+ > Gkilis, P. (2026). *Reconstruction history, not synthesis: benign processing moves frozen
47
+ > detectors on genuine human speech.* Zenodo.
48
+ > [10.5281/zenodo.22819223](https://doi.org/10.5281/zenodo.22819223) · CC BY 4.0
49
+
50
+ ⚠️ Read its scope limits before quoting it: three speakers, 47 utterances, neutral speech,
51
+ one language; seven of the thirteen detectors come from a single research group; **no
52
+ mechanism is identified and no listening test was run.** Thirteen of that paper's own claims
53
+ were withdrawn during three rounds of independent adversarial audit, and the withdrawals are
54
+ published in it.
55
+
56
+ So a detector score is not interpretable without knowing the provenance of the audio it was
57
+ computed on — and before any of that, you need to know which detectors on your panel can do
58
+ the task on your material at all. That is what `panel` answers.
59
+
60
+ ## Usage
61
+
62
+ ```
63
+ detectorproof panel \
64
+ --panel panel.json \
65
+ --scores scores.csv \
66
+ --labels labels.csv \
67
+ --condition CTRL
68
+ ```
69
+
70
+ **`panel.json`** — the declaration, fixed before the run.
71
+
72
+ ```json
73
+ [
74
+ {"name": "det-a", "orientation": "higher_bonafide", "identity": "sha256:1f3a...", "source": "..."},
75
+ {"name": "det-b", "orientation": "higher_synthetic", "identity": "rev:c1dbe2ae"},
76
+ {"name": "det-c", "orientation": "unresolved", "identity": "commit:72c32e28"}
77
+ ]
78
+ ```
79
+
80
+ `orientation` is resolved from the model's own documentation or training-label path, never
81
+ from what makes your benchmark look right. `unresolved` is a first-class value: the detector
82
+ is scored and reported, and its *sign* is not interpreted.
83
+
84
+ `identity` pins the weights — a revision, a commit, a file hash. It is required. A panel that
85
+ cannot say which checkpoint produced a number is not frozen.
86
+
87
+ **`scores.csv`** — `detector,item,condition,score`. One row per detector per item per
88
+ condition. Scores are in each detector's native units. A missing measurement is written
89
+ `NOT_MEASURED` (or `n/a`), never `0`.
90
+
91
+ **`labels.csv`** — `item,label`, where label is `bonafide` or `synthetic`.
92
+
93
+ In Python:
94
+
95
+ ```python
96
+ from detectorproof import Detector, run_panel, render
97
+
98
+ report = run_panel(detectors, scores, labels, condition="CTRL")
99
+ print(render(report))
100
+
101
+ for result in report.competent_detectors(): # the only set you may aggregate over
102
+ ...
103
+ ```
104
+
105
+ ## The refusals
106
+
107
+ These are enforced in code, not described here. Each was bought by a specific measured
108
+ failure.
109
+
110
+ | refusal | the failure that bought it |
111
+ |---|---|
112
+ | 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 |
113
+ | 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 |
114
+ | 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% |
115
+ | **NOT MEASURED is a third state** — never zero, never an absence of effect | a missing measurement reported as zero is worse than reporting nothing |
116
+ | 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 |
117
+
118
+ There is deliberately **no function that recommends a transformation** to move a score, and
119
+ there will not be one. In the work this came from, an intervention tuned against a chosen
120
+ detector statistic made the audio *more* separable, not less — one detector's AUC moved from
121
+ 0.576 to 0.763. The instrument reports; it does not advise. That refusal is the point, not an
122
+ omission.
123
+
124
+ ## What a verdict means
125
+
126
+ `NOT COMPETENT ON THIS MATERIAL` is a statement about **that checkpoint on your control
127
+ set**. It is never a statement about a detector's general quality. Every detector was trained
128
+ by someone else, on other data, for other purposes.
129
+
130
+ An empty competent set is a reportable result about your panel and your material. It is
131
+ **not** evidence that your audio is genuine, and it is not evidence that detection is
132
+ impossible. Absence of evidence is not evidence of absence, and nothing downstream may run on
133
+ an empty competent set.
134
+
135
+ ## Design
136
+
137
+ Zero dependencies in the core. Ties are handled by midranks everywhere, because a detector
138
+ whose scores compress near zero produces long runs of equal values and a rank statistic that
139
+ breaks ties arbitrarily reports a different answer depending on the order its input arrived
140
+ in. AUC is computed from the rank sum rather than by counting pairs.
141
+
142
+ Every field this package reads has a **declared domain** — its type, its admissible values,
143
+ and what absence means. Validation asks whether a value is inside that declaration; it does
144
+ not carry a list of bad values anyone has seen. That list is unbounded and the domain is not.
145
+
146
+ ## The family
147
+
148
+ `trainproof` (training runs) · `ttsproof` (TTS failure modes) · `spkproof` (speaker-verification
149
+ false rejection) · **`detectorproof`** (detector competence and robustness).
150
+
151
+ "Did the processing preserve speaker identity?" is a different question with different
152
+ statistics, and `spkproof` answers it. **Version 0.1.0 has no dependency on `spkproof` and does
153
+ not call it** — the two are used side by side, not chained.
154
+
155
+ ## Status
156
+
157
+ `panel` is built and tested. `probe`, `identity` and `falsepositive` are specified and not
158
+ built. They are deliberately not built at the same time — `panel` is the gate the other three
159
+ depend on, and it has to refuse correctly on a detector already known to be incompetent
160
+ before anything is built on top of it.
161
+
162
+ ## Licence
163
+
164
+ MIT.