Systems and subsystems

Detector contract and safe runner

Every detector takes the same read-only clip and returns the same kind of result: a score that rises with synthetic likelihood, one sentence of evidence, named features and a status. A wrapper guarantees that a crashing detector becomes a neutral, flagged result and never costs a file its row.

In plain words

Ten detectors from four people had to plug into one pipeline in a weekend. The contract is the agreement that made that possible: what a detector gets, what it must return, and what happens when it fails. The rules are enforced, not assumed: a score outside [0, 1] or a missing evidence sentence raises an error rather than being quietly fixed, so a detector cannot slip a bad value into fusion.

What goes in and comes out

  • In: a ClipContext: the path, the decoded audio (read-only, decoded once), the header probe (computed once), and a per-clip memo cache that detectors share expensive intermediates through.
  • Out: a DetectorResult(name, score, evidence, features, status, error).
  • Through: safe_run(detector, ctx), which never raises.

Where it lives

src/hearsay/detectors/base.py: ClipContext, DetectorResult, the Detector protocol, safe_run, and the Registry. The engineered detectors register themselves through engineered.py.

Rules and where each is set

  • score must be a real number, not a bool, finite, in [0, 1]; violations raise ValueError and values are never clipped (base.py, lines 125–127).
  • evidence must be a non-empty string; features a mapping of non-empty names to finite floats (copied, so a detector cannot mutate a result after the fact); status one of ok, skipped, error, and only an error carries an error string (base.py, lines 128–143).
  • The neutral score is 0.5source: src/hearsay/detectors/base.py line 40 (GitHub; assumes main is public). safe_run turns a raised exception, a wrong return type, a name mismatch or a non-ok status from run into status="error" at that score with the evidence "detector failed: …"; a detector whose applies() is false is skipped without running (base.py, lines 163–192). Keyboard interrupts still propagate.
  • The audio is decoded once and marked read-only; a decode failure is cached and re-raised without a second FFmpeg call (base.py, lines 70–84).
  • The registry rejects duplicate or empty names and iterates in sorted name order, so fusion columns are stable (base.py, lines 195–217).
  • Detectors never read filenames or filesystem timestamps, because git, zip and Docker rewrite them (a project rule in CLAUDE.md).

Status

Built and frozen. A change to DetectorResult fields must name the affected detector owners; none was made after the D-track began.

Numbers

Every learned detector exports one file per detector with columns path, fold, split, score and logit over the same 21,671source: README.md, section the-detector-contract rows (the training clips and the test files); fusion reads only those exports, so any detector can be swapped or ablated without touching the others.

Known limits

The neutral 0.5source: src/hearsay/detectors/base.py line 40 (GitHub; assumes main is public) on a skipped or errored result is a placeholder, not evidence: the fusion code imputes such a column at its training centre instead (see the fusion rule). The role labels in the runner's code tag Spectra-AASIST as "fused" although the rule only lets it suppress; the reports and this site say "suppressor".

Tests that pin it

tests/test_detector_contract.py, thirty-odd cases named B1–B9: invalid scores rejected and never clipped, valid scores coerced to float, score direction, exceptions become error results, bad return values become errors, evidence required, bad features rejected, registry rejects duplicates and sorts, the same clip scored twice is identical, audio is read-only, decoded once, decode failure cached, memo computes once, not-applicable is skipped, results unhashable but comparable, status and error coherence. tests/test_engineered_registry.py pins the seven registered engineered names.

Sources

CLAUDE.md, "Detector contract"; Architecture §5; the README.