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.
- detectorproof-0.1.0/.github/workflows/ci.yml +152 -0
- detectorproof-0.1.0/.github/workflows/release.yml +98 -0
- detectorproof-0.1.0/CHANGELOG.md +32 -0
- detectorproof-0.1.0/LICENSE +21 -0
- detectorproof-0.1.0/MANIFEST.in +15 -0
- detectorproof-0.1.0/PKG-INFO +192 -0
- detectorproof-0.1.0/README.md +164 -0
- detectorproof-0.1.0/pyproject.toml +71 -0
- detectorproof-0.1.0/scripts/ci.py +615 -0
- detectorproof-0.1.0/setup.cfg +4 -0
- detectorproof-0.1.0/src/detectorproof/__init__.py +101 -0
- detectorproof-0.1.0/src/detectorproof/cli.py +153 -0
- detectorproof-0.1.0/src/detectorproof/domain.py +314 -0
- detectorproof-0.1.0/src/detectorproof/panel.py +434 -0
- detectorproof-0.1.0/src/detectorproof/report.py +102 -0
- detectorproof-0.1.0/src/detectorproof/stats.py +92 -0
- detectorproof-0.1.0/src/detectorproof.egg-info/PKG-INFO +192 -0
- detectorproof-0.1.0/src/detectorproof.egg-info/SOURCES.txt +25 -0
- detectorproof-0.1.0/src/detectorproof.egg-info/dependency_links.txt +1 -0
- detectorproof-0.1.0/src/detectorproof.egg-info/entry_points.txt +2 -0
- detectorproof-0.1.0/src/detectorproof.egg-info/requires.txt +7 -0
- detectorproof-0.1.0/src/detectorproof.egg-info/top_level.txt +1 -0
- detectorproof-0.1.0/tests/test_audit_regressions.py +621 -0
- detectorproof-0.1.0/tests/test_domain.py +115 -0
- detectorproof-0.1.0/tests/test_fuzz.py +241 -0
- detectorproof-0.1.0/tests/test_refusals.py +197 -0
- detectorproof-0.1.0/tests/test_stats.py +73 -0
|
@@ -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.
|