Systems and subsystems
Audio loader and probe
One decode path for every file: FFmpeg turns any container or codec into 16 kHz mono float32, once, and the header is read separately. Nothing downstream ever sees the original format.
In plain words
Every audio file, whatever it is, becomes the same thing before any detector looks at it: a single channel of samples at 16 kHzsource: README.md, section how-it-decides, as floating-point numbers, with silence trimmed where a detector asks for it. The header (codec, rate, tags) is read on its own and never mixed into the audio. This is what closes the containerWhat the file header says: codec, sample rate, bit depth, encoder tags. Read for routing; never learned from, because on our training data the container is the label. shortcutA difference between training and test data that a model can exploit instead of listening to the voice: peak level, leading silence, clip length, container fields, a missing high band. Each one found was closed before it could be learned.: training real speech is FLAC or 22 kHz WAV and some training fakes are MP3, while every test file is 16 kHz PCM, so a model that could see the format would learn the format.
What goes in and comes out
load_audio(path)→ a 1-D float32 array at 16 kHz mono.probe_audio(path)→ a dict of header facts: format, codec, sample rate, channels, duration, bit rate; on failure anerrorentry, never an exception.trim_silence(x)→ the array with quiet edges removed, relative to its loudest frame.windows(x, win, hop)→ fixed-length windows, tiling clips shorter than one window.
Where it lives
src/hearsay/audio.py: DecodeError, load_audio, probe_audio, windows, trim_silence. The sample rate constant is in the package's __init__.
Parameters and where each is set
- The FFmpeg command takes the first audio stream, downmixes to mono and resamples to 16 kHz, emitting little-endian float32 (audio.py, lines 31–36). Empty output raises
DecodeErrorwith FFmpeg's last line; a partial decode is kept; non-finite samples are zeroed. - No denoising and no loudness normalization anywhere on the path; a test pins it. Level is normalized per detector, later.
- Silence trim: threshold 35source: src/hearsay/audio.py line 90 (GitHub; assumes main is public) dB below the loudest 20 ms frame, and the result is never shorter than 1.0source: src/hearsay/audio.py line 91 (GitHub; assumes main is public) s; with fewer than two frames or nothing above the threshold the clip is returned unchanged (audio.py, lines 90–107).
- Windows: the hop defaults to half a window; a clip shorter than one window is tiled; the last window is flush with the end (audio.py, lines 73–87). Windows are the v1 path; the deep detectors now use segment mode (see segment preparation).
- Probe:
ffprobewith-show_format -show_streams -select_streams a:0; returns the fields above and never raises (audio.py, lines 44–70).
Status
Built and tested; unchanged since the M0 rung. Invariant 1 of the architecture document ("one audio path").
Numbers
None of its own. Its effect shows up as the closed container shortcut on What did not work.
Known limits
FFmpeg's channel downmix averages channels; a stereo file with out-of-phase channels would lose signal. The loader keeps a partial decode of a truncated file rather than failing it, so a damaged file scores on what could be read (and the runner flags decode failures separately).
Tests that pin it
tests/test_audio_and_submission.py: any format to 16 kHz mono float32 (stereo 44.1k WAV, 8k mono, 22 kHz, CBR and VBR MP3, AAC in m4a and mp4, FLAC, Opus), determinism, empty and garbage files raise, a truncated WAV decodes what it can, NaN samples are sanitized, silence loads as zeros, no loudness normalization, windows cover the clip, silence trim is level-independent. tests/test_detector_contract.py pins that a clip is decoded once and that a decode failure is cached.
Sources
Architecture, invariant 1; the shortcut ledger; the code map.