The library
Rendered from docs/reports/2026-09-26_m3-spectra.md; the markdown file is the record.
On this page
- What M3 is
- Input path (the same function scores every row)
- Pilot: choosing the short-clip mode (CPU, 05:02–05:09)
- Direction baseline (untouched scripts/spectradirection.py, raw path: no trim, no crop, no band match, repeat-pad)
- Peak-normalization arm (rejected)
- In-the-Wild stress set (3,000 clips, 2,000 real / 1,000 spoof, seed-200 crops, zero)
- Full export: outer holdout, test set, timing (MPS, 05:22–06:00)
- What Spectra is blind to, and what this rung does not test
- Notes for fusion (M4)
- Timing
- Reproduce
M3: Spectra-AASIST as the second deep score stream (Sat Sep 26, 2026)
Run spec: docs/specs/2026-09-26_m3-spectra-aasist.md. Plan and Codex review: docs/reports/2026-09-26_m3-spectra-aasist-plan{,-review}.md. Code: src/hearsay/spectra.py, scripts/score_spectra.py, tests/test_spectra.py (40 collected: 39 hermetic plus one weights-gated CPU test). Export for fusion: outputs/detector_scores/spectra_aasist.csv (path, fold, split, score, logit; logit is the raw model margin, score its sigmoid, nothing fitted), sidecar spectra_aasist_summary.json, raw companion outputs/spectra/spectra_aasist_raw.csv, run directory models/m3_spectra_<stamp>/.
What M3 is
Spectra-AASIST (lab260/Spectra-AASIST, Hugging Face; XLS-R 300M front end, AASIST graph-attention back end, trained by its authors on undisclosed data) run off the shelf, with no training, over the NSA fold file (20,000 rows) and the test set (1,671 files). Its value for fusion is a second deep score whose training differs from the M1 probe, not a higher number than M1. The model's margin synth_logit = logit_spoof − logit_bonafide increases with synthetic likelihood; direction is checked on NSA inner rows before anything is exported.
Input path (the same function scores every row)
load_audio (16 kHz mono) → hearsay.embed.prepare_segment with default arguments, i.e. the shared band match (7.25 kHz Kaiser low-pass matching the test set's 7.2 kHz wall, commit 133e536), silence trim, optional test-length crop, cap at 8 s → short-clip handling → 64,600-sample windows at 50% hop → pre-emphasis → model → mean of the window logits.
Crop parity. Fold rows (inner and holdout) are cropped to test-duration lengths with exactly the draws M1 and the handcrafted detector used (test_duration_sampler(0), one draw per nsa_train_sample.csv row in order, offset seed = row index), so fusion sees the same audio per file across all three detectors. Test rows are scored whole. Without the crops, four in five test rows would be padded and almost no training row would be, which is the tiling seam M1 v2 removed.
Short clips. 81% of NSA test clips are shorter than one Spectra window (4.04 s; test median 3.41 s), so how a short clip is padded is the test condition. Three modes were measured on inner rows only: repeat (tile to one window, the model card's own pad_random, in-distribution for the model but a seam), zero (zero-pad to one window), whole (the clip at its own length; the AASIST positional table covers 8 s).
Pilot: choosing the short-clip mode (CPU, 05:02–05:09)
First 400 fold rows (322 inner rows, 171 bona fide / 151 spoof; holdout rows skipped) plus the first 200 test rows, all three modes in one decode pass, 401 s at 0.77 s per clip on the loaded CPU (three forwards per short clip). No decode or model failures. 434 of 522 prepared clips were shorter than one window.
| Mode | Inner minDCF (π_synth = 0.3) | Inner EER | Inner AUC | Spoof / bona fide median margin | Test share with margin > 0 (prior ≈ 0.30) | Test share under the card's threshold |
|---|---|---|---|---|---|---|
| repeat | 0.0066 | 0.6% | 1.000 | +10.2 / −7.7 | 0.310 | 0.290 |
| zero | 0.0000 | 0.0% | 1.000 | +10.4 / −6.5 | 0.265 | 0.260 |
| whole | 0.0000 | 0.0% | 1.000 | +11.5 / −7.3 | 0.280 | 0.275 |
Selection rule (frozen in P2): inner minDCF, ties within 0.02 broken by inner AUC, then zero. All three tied, so zero was chosen, the handoff's option (b). Two honest caveats: 322 rows make one bona fide false alarm worth 0.055 in normalized minDCF, so the pilot can only say "all three are near-perfect on inner rows"; and repeat is the in-distribution choice for the model while zero and whole are out of distribution, which the numbers did not punish here. Per generator on the inner rows, every DiffSSD generator scored minDCF 0.0 in zero mode, including grad_tts, pro_diff and ElevenLabs, the three the handcrafted detector is blind to.
Level. Spectra does not level-normalize its input. Within the bona fide class the margin is uncorrelated with clip peak level (Spearman 0.00), within the spoof class it rises with level (Spearman 0.36 for zero, 0.27–0.29 for the others). Level is doing some of the work on the synthetic side; it cannot create a false alarm on quiet real speech, but it is a shortcut to keep in mind if the test set's synthetic clips were normalized differently from DiffSSD.
Direction baseline (untouched scripts/spectra_direction.py, raw path: no trim, no crop, no band match, repeat-pad)
300 inner rows (150 per class, seed 0, outputs/manifests/nsa_inner_direction300.csv), MPS, 05:19: AUC 1.000, EER 0.0%, median margin +10.5 for spoof and −7.7 for bona fide, EER threshold at a margin of 6.5, 0.43 s per clip (uncapped 5–9 s clips, up to 16 windows). The script's own gate passed. Raw scores: outputs/spectra/nsa_inner300_scores.csv. So the direction and the separation do not depend on the shared input path; the path is there for parity with the other detectors and for the test set's band, not to make Spectra work.
Peak-normalization arm (rejected)
After the pilot's level flag, the main chat proposed scaling every prepared clip to peak 0.95 (Spectra sets normalize_waveform=False; training real audio peaks about 0.5, NSA test clips and some spoof generators about 1.0). The arm ran on MPS on the same 322 inner and 200 test rows, zero mode (--peak-norm, run outputs/spectra/pilot/20260926-0517_pilot_peaknorm):
| Arm | Inner minDCF | Inner EER | Inner AUC | LJ Speech bona fide minDCF | Test share with margin > 0 | Within-spoof Spearman(peak, margin) |
|---|---|---|---|---|---|---|
| raw (shipped) | 0.0000 | 0.0% | 1.0000 | 0.000 | 0.265 | 0.36 |
| peak 0.95 | 0.0331 | 0.6% | 0.9997 | 0.033 | 0.265 | 0.40 |
Rejected by the rule recorded in the spec (outside the 0.02 tie band, and the correlation did not drop). The correlation with the pre-normalization peak survived normalization, so level is a proxy for something else (the loud generators are also the easy ones), not the cue the model uses; normalizing only cost a little on the real side. Shipped configuration: zero mode, band match from prepare_segment, no level normalization.
In-the-Wild stress set (3,000 clips, 2,000 real / 1,000 spoof, seed-200 crops, zero)
outputs/manifests/itw_stress.csv scored on CPU (1,086 s, 0.36 s per clip, no failures), band-matched, test-length crops, zero-pad, no level normalization; the same function that scores every NSA row. Run: outputs/spectra/itw_stress/{scores.csv, meta.json}.
| Spectra (M3) | M1 v3 (band-matched probe) | M1b v3 (+ASVspoof bona fide) | |
|---|---|---|---|
| In-the-Wild minDCF (π_synth = 0.3) | 0.065 | 0.380 | 0.342 |
| In-the-Wild EER | 3.2% | ||
| AUC | 0.996 | ||
| Real P_FA at the detector's own inner-row threshold | 0.0% (0 of 2,000) | 0.7% | 0.8% |
| Spoof P_miss at that threshold | 7.8% (EER threshold) / 13.8% (minDCF threshold) | ||
| Median margin, spoof / real | +9.8 / −3.2 |
The threshold is Spectra's own, not M1's number (the score spaces differ). Final values from the full run's 16,142 inner rows: the minDCF-optimal margin is 6.92 (real P_FA 0.00%, spoof P_miss 13.80%) and the EER margin is 5.41 (P_FA 0.00%, P_miss 7.80%); the table's row uses the EER threshold, which is what the pilot's minDCF sweep also landed on. Readouts: outputs/spectra/itw_stress/readout_{provisional,final}.json. The real side of In-the-Wild sits closer to the boundary (median −3.2) than NSA real speech does (−6.5 to −7.7), which is the channel shift this set is for, and the margin still leaves zero false alarms. M1's numbers are the main chat's, on the same 3,000 clips.
Flag: possibly optimistic. The model card lists In-the-Wild only as an evaluation set (the authors report 1.46% EER on it) and does not disclose the training data at all (arena tier "unpublished / proprietary"), so this readout cannot be shown to be uncontaminated. The NSA holdout and the test-set share below are the evidence that does not depend on that.
Full export: outer holdout, test set, timing (MPS, 05:22–06:00)
Run models/m3_spectra_20260926-0522 (config hash d7138ffc8569…, git 47c106f6): 21,671 rows, 0 decode or model failures in any split, 2256 s at 0.104 s per clip on MPS (decode in 4 threads, forward on the GPU). Published 06:00:23.
Outer holdout (3,858 rows: the playht and wavegrad2 generators plus 26 real-speech speaker groups, never seen by any of our fitting, and not selected on: read once). The comparator is the band-matched M1 v3 probe on the same fold file (models/m1_wav2vec2-xls-r-300m_L7_20260926-0518).
| Holdout reading | Spectra (M3) | M1 v3 |
|---|---|---|
| minDCF, π_synth = 0.3 (the judged number) | 0.0120 | 0.1593 |
| EER | 0.16% | 2.85% |
| AUC | 1.0000 | 0.9959 |
| Actual DCF at the Bayes threshold on the raw margin | 0.0653 | 0.1679 |
| minDCF at π = 0.5 | 0.0063 | 0.1087 |
| Sponsor code as shipped (higher = bona fide), as-is / flipped | 1.0000 / 0.0022 | 1.0000 / 0.1051 |
The sponsor-code reading confirms the direction question is the sponsor's, not ours: as-is our synthetic-high scores read as the worst possible file, flipped they read near-perfect. We submit 1.0 = synthetic and never flip (docs/plan.md).
Per held-out generator (that generator's spoofs against every holdout bona fide clip) and per real source (that source's bona fide clips against every holdout spoof), the by_group shape the handcrafted trainer uses:
| Group | Spectra minDCF | Spectra EER | M1 v3 minDCF | M1 v3 EER |
|---|---|---|---|---|
| playht (holdout generator, MP3 source) | 0.0100 | 0.18% | 0.0350 | 0.60% |
| wavegrad2 (holdout generator) | 0.0120 | 0.21% | 0.2304 | 3.89% |
| LibriSpeech real (held-out speakers) | 0.0111 | 0.13% | 0.2050 | 3.80% |
| LJ Speech real | 0.0080 | 0.21% | 0.0832 | 1.94% |
One fold-file limitation applies to every detector, not only M3: LJ Speech is grouped by chapter, so its single speaker appears in both inner and holdout rows. The LJ real row above is held out by corpus slice, not by speaker; the LibriSpeech row is the speaker-held-out one.
Inner rows, pooled (16,142 rows; a diagnostic, and possibly in-sample for Spectra, see the caveat below): minDCF 0.0045, EER 0.11%, AUC 1.0000. Per generator: diffgan_tts 0.0000, elevenlabs 0.0173, grad_tts 0.0031, openvoicev2 0.0010, pro_diff 0.0010, unit_speech 0.0010, xtts_v2 0.0010, your_tts 0.0000. Per real source: LibriSpeech 0.0025, LJ Speech 0.0048. ElevenLabs is the hardest inner generator for Spectra as it is for the handcrafted detector, at a level three orders of magnitude lower.
Test set (1,671 files, unlabeled; the sanity check that does not depend on our corpora): 29.26% of files have a positive margin (prior about 30%; M1 v3 28.9%, M1b v3 26.8% after band matching), 28.31% sit under the model card's own classify threshold, and the in-sample Platt diagnostic would put 27.41% above one half. Score mean 0.299, std 0.448, 1671 distinct values. Margins run -9.36 to +11.95 across all rows.
Disagreement with M1 v3 on the test set (submissions/20260926-0522_M1_…0518.tsv, joined on filename, 1671 files): Spearman between the two score columns 0.569. At each detector's own midpoint (M1 probability 0.5, Spectra zero margin): both call synthetic 409, both real 1108, Spectra only 80, M1 only 74: 154 files (9.22%) where the two deep detectors disagree. A rank correlation of only 0.57 between two detectors that both separate the holdout well is the point of M3: they are wrong on different files, and a stacker can use that.
Level on all inner rows: within-spoof Spearman(peak, margin) 0.31, within-bona fide -0.07; the same picture as the pilot, and the peak-normalization arm above showed it is a proxy, not the cue.
Caveat: the inner rows may be in-sample for Spectra. The model's training data is undisclosed; LJ Speech, LibriSpeech and DiffSSD are public and plausible training corpora, and the inner rows score AUC 1.0. meta.json records caveats.inner_rows_possibly_in_sample = true. Fusion should not treat the inner_oof column of this detector as out-of-fold evidence the way it does for M1 and the handcrafted detector; the holdout (also DiffSSD and public real speech, so not immune), the test-set share and In-the-Wild are the readings that depend least on it. The draft-review score is the only fully independent check.
What Spectra is blind to, and what this rung does not test
On our data Spectra trails M1 nowhere: every holdout group is at or under 0.012 minDCF. What it is blind to, we cannot see from labeled data we own: the two deep detectors disagree on 154 test files, the real side of In-the-Wild sits within 3.2 margin units of the boundary (median −3.2 against −6.3 on NSA real speech), and 13.8% of In-the-Wild spoofs fall under the NSA-derived minDCF threshold, so unseen generators under channel shift are where misses will come from. The level proxy (loud synthetic generators are the easy ones) is the one measured shortcut.
Not tested here, by construction of a scoring rung with nothing trained: robustness to codec laundering, telephony band-limiting and simulated replay (M5's augmentation is where those are measured), and physical replay (no hardware). The band match and the In-the-Wild readout are the only channel-shift evidence in this report.
Notes for fusion (M4)
- Read the
logitcolumn directly and standardize it per detector on inner rows (commit 944380c). Spectra's margins run about ±10 to ±17;logit(clip(score, 1e-4))would saturate them to ±9.2 and lose the ranking in the bona fide tail where the minDCF threshold lives. inner_oofrows are not out-of-fold in any learned sense: nothing was fit locally, but whether Spectra's own training overlapped our inner or holdout corpora is unknown (see the caveat above), so neither split is guaranteed out-of-sample for this model. The label keeps the file shape uniform with the learned detectors; do not weight this column as out-of-fold evidence.- NaN rows (decode or model failures) stay NaN in both columns for imputation and are counted in the summary sidecar.
Timing
| Stage | Device | Clips | Seconds | s per clip |
|---|---|---|---|---|
| Pad-mode pilot, 3 modes in one pass | CPU (loaded, 4 threads, nice 10) | 522 | 401 | 0.77 |
| Peak-norm arm | MPS | 522 | 81 | 0.16 |
| Direction baseline (raw path, uncapped) | MPS | 300 | 129 | 0.43 |
| In-the-Wild stress | CPU (loaded) | 3,000 | 1,086 | 0.36 |
| Full export | MPS | 21,671 | 2256 | 0.104 |
Wall clock: P2 frozen 04:20, build and tests 04:50–05:02, pilot 05:02–05:09, GPU released by the main chat 05:17, export 05:22–06:00, report and commit after 06:05. The 45-minute box in the ladder covered the pilot and the export; the frozen P2 test list (37 tests) took the build past it, as the run spec records.
Reproduce
nice -n 10 uv run python scripts/score_spectra.py --limit 400 --test-limit 200 --pad-modes repeat,zero,whole --device cpu --threads 4 --name pilot
nice -n 10 uv run python scripts/score_spectra.py --manifest outputs/manifests/itw_stress.csv --name itw_stress --pad-mode zero --device cpu --threads 4
uv run python scripts/score_spectra.py --readout inner=outputs/spectra/pilot/<stamp>_pilot/scores_zero.csv stress=itw=outputs/spectra/itw_stress/scores.csv --out outputs/spectra/itw_stress/readout_provisional.json
nohup nice -n 10 uv run python scripts/score_spectra.py --pad-mode zero --device mps > outputs/spectra/score_spectra.log 2>&1 &
uv run python scripts/score_spectra.py --readout inner=outputs/spectra/spectra_aasist_raw.csv test=outputs/spectra/spectra_aasist_raw.csv stress=itw=outputs/spectra/itw_stress/scores.csv m1_tsv=submissions/<M1 v3 tsv> pilot=outputs/spectra/pilot/<stamp>_pilot/meta.json --final --out outputs/spectra/itw_stress/readout_final.json --merge-into models/m3_spectra_<stamp>/meta.json
The last command is how the In-the-Wild block, the M1 disagreement, the pilot table and the caveats got into the run's meta.json (as its readout block); the export itself ran without --stress so its end could not depend on a file another process was still writing. --resume restarts an interrupted full run from its checkpoint; --max-chunks 1 is a dry run. At risk (gitignored): outputs/detector_scores/spectra_aasist*.csv, outputs/spectra/, models/m3_spectra_*/.