File size: 16,032 Bytes
6cf377a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | # Sound Event Detection
Run using code available on [`Github`](https://github.com/earthspecies/sound-event-detection)
Pretrained sound event detection models focused on bioacoustics. Supports three main functions:
- Inference with pre-trained models: Within python, via a script, or via the large-scale inference (LSI) pipeline.
- Evaluation of model performance on detection datasets.
- Load pre-computed model detections for datasets like Xeno-Canto and iNaturalist.
## Installation
Requires [`uv`](https://docs.astral.sh/uv/). Installation may take several minutes. GPU is not required but will improve speed.
Required packages are listed in `pyproject.toml`. To install them, run:
```bash
uv sync --group gpu # omit --group gpu for CPU-only
```
All commands run through `uv run`. It may be necessary to include `--group gpu` if using a GPU. Evaluation and LSI also need the dataset storage referenced by `configs/data/*.yml`.
Large-scale inference and using precomputed selection tables both require [`alp-data`](https://github.com/earthspecies/alp-data/), which is already included in `pyproject.toml`.
## Quick start β BirdCODE over a folder of audio
Run the pretrained BirdCODE detector (loaded from the Hub) over every audio file in a folder β any sample rate, resampled to 32 kHz as needed β and write a selection table next to each recording: `dir/x.wav` β `dir/BirdCODE_predictions/x.txt`. Currently supports wav, flac, ogg, and mp3.
```bash
uv run sed-folder --folder /path/to/audio
```
Two short demo recordings are provided. To run BirdCODE on them, do:
```bash
uv run sed-folder --folder tests/samples/demo/audio
```
This writes `tests/samples/demo/audio/BirdCODE_predictions/{20230730,20260623}.txt`, which should match the tables in `tests/samples/demo/output_expected/`. On CPU it takes roughly 1.5 minutes after the model weights (~1.1 GB) are downloaded.
Postprocessing is applied: By default, per-frame detections are thresholded at 0.5, boxes with the same label are merged if separated by less than 1 second, and non-maximal suppression is applied with an IoU threshold of 0.8. Geography filtering is off by default; enable it with `--geo-filter`, a directory of `*.gpkg` range maps, and the recording site's coordinates (applied to every file):
```bash
uv run sed-folder --folder /path/to/audio \
--geo-filter --range-map-dir geography/range_maps \
--latitude 42.5 --longitude -72.2
```
## Official models
| Model | Publication | Checkpoint | Summary |
|---|---|---|---|
| BirdCODE | TODO | [EarthSpeciesProject/sed-birdcode](https://huggingface.co/EarthSpeciesProject/sed-birdcode) | Bird Communication Detector |
## CLI entry points
| Command | Purpose | Assumes running |
|---|---|---|
| `sed-folder` | Run BirdCODE over a folder of audio β selection tables | β (loads the model in-process) |
| `sed-server` | Serve a frame detector or sliding-window detector | backing classifier server (sliding-window only) |
| `sed-denoising-server` | Serve the denoising detector | a detector server + a separator server |
| `sed-eval` | Run an evaluation against a served model | a `sed-server` / `sed-denoising-server` server |
| `sed-lsi` | Large-scale inference over a dataset | a `sed-server` (`preds`) or `sed-denoising-server` (`denoised`/`stems`) server |
| `sed-lsi-postprocess` | Turn LSI predictions into selection tables | β (reads shards) |
| `sed-lsi-features` | Add per-event acoustic features to selection tables | β (reads shards) |
Every CLI has a `describe` subcommand that prints its config schema(s), e.g. `uv run sed-eval describe`.
## Using BirdCODE in Python
`FrameDetector` loads a trained detector in-process, either from the HuggingFace Hub by repo id or from a checkpoint directory (local, `gs://β¦`, or `r2://β¦`):
```python
from sound_event_detection.models import FrameDetector
# From the HuggingFace Hub (downloads the snapshot, then rebuilds the model);
birdcode = FrameDetector.from_hf_hub("EarthSpeciesProject/sed-birdcode").eval().to("cuda")
# Or from a checkpoint directory: weights from best_model.pt, labels from
# labels.txt, architecture from config.yaml.
ckpt = "checkpoints/birdcode_esp_research"
birdcode = FrameDetector.from_checkpoint_dir(ckpt, f"{ckpt}/config.yaml").to("cuda")
out = birdcode.run(audio, overlap=0.5) # audio: np.ndarray [batch, samples] at 32 kHz
out.predictions # [batch, time, classes] probabilities in [0, 1]
out.class_names # list[str] labels aligned to the classes axis
```
## Serving models
For large-scale inference and evaluation, we serve the model over HTTP, then point a client CLI at it via an http-client config.
A **model config** YAML tells the server what to load, dispatching on `type`. The unified server (`sed-server`) reads its path from the `SED_MODEL_CONFIG` environment variable.
### Frame detectors β `type: frame`
Trained detectors (BirdCODE and ablations) loaded either from the HuggingFace Hub or from a local checkpoint directory. All current checkpoints run at 32 kHz.
Set `hf_repo_id` to download and serve a checkpoint from the Hub β this is how the example config loads BirdCODE. An optional `revision` pins a branch, tag, or commit (defaults to the repo's default branch):
```yaml
type: frame
hf_repo_id: EarthSpeciesProject/sed-birdcode
# revision: main # optional
```
Alternatively, `model_folder` serves a local checkpoint directory (expects `config.yaml`, `best_model.pt`, and `labels.txt`).
Serve either config the same way:
```bash
SED_MODEL_CONFIG=configs/birdcode/models/birdcode_esp_research.yml \
uv run sed-server --host 0.0.0.0 --port 8100
```
`sed-server` accepts `--host` (default `localhost`), `--port` (default `8100`), `--workers`, `--reload`, and `--log-level`. `SED_DEVICE=cpu|cuda` selects the device (default: cuda if available).
Ablation checkpoints use the same `type: frame` shape:
`configs/birdcode/models/ablations/`.
### Sliding-window detectors β `type: perch2 | audioprotopnet | beats_sl_all`
Clip classifiers wrapped in a `SlidingWindowDetector` to produce frame-level predictions. Each needs a **backing classifier server** already running, discovered through `addr_file` (a text file containing `host:port`):
```yaml
type: audioprotopnet
addr_file: ~/audioprotopnet-server/server.addr
window_size: 5.0 # seconds
hop_size: 2.0 # seconds
analysis_window: 2.0 # optional; defaults to window_size
```
| Type | Backing server | Sample rate |
|---|---|---|
| `perch2` | [earthspecies/perch2-server](https://github.com/earthspecies/perch2-server) | 32 kHz |
| `audioprotopnet` | [earthspecies/audioprotopnet-server](https://github.com/earthspecies/audioprotopnet-server) | 32 kHz |
| `beats_sl_all` | in-repo (below) | 16 kHz |
The external servers write their own `server.addr`; point the config's `addr_file` at it. Serve the wrapper the same way as a frame detector:
```bash
SED_MODEL_CONFIG=configs/birdcode/models/baselines/audioprotopnet_2s.yml \
uv run sed-server --port 8100
```
`beats_sl_all` runs at 16 kHz β evaluate it with `frame_eval_16k.yml` (frame detection) or `birdset_clip_eval_16k.yml` (clip classification). Its backing classifier is served in-repo:
```bash
# 1. backing classifier (16 kHz), then record its host:port
SED_DEVICE=cuda uv run uvicorn \
sound_event_detection.serving.sl_beats_all_server:app --host 0.0.0.0 --port 8200
echo "HOST:8200" > .server_addrs/beats_sl_all.addr # path the config's addr_file points at
# 2. the sliding-window wrapper
SED_MODEL_CONFIG=configs/birdcode/models/baselines/beats_sl_all_2s.yml \
uv run sed-server --port 8100
```
### Denoising detector β `type: denoising_detector`
NOTE: This requires a separator server to be running. Separator server code will be provided at a later date.
Wraps a detector client and a source-separator client, adding `POST /separate_and_detect` (used by LSI) to the standard contract. Both backing servers must be up when it starts. Its model config names them as pure http-client configs:
```yaml
type: denoising_detector
detector: {url: http://localhost:8100, timeout: 300} # a sed-server detector server
separator: {url: http://localhost:8200, timeout: 300} # a separator server
threshold: 0.5
resampling_method: torchaudio_kaiser_fast
```
```bash
# with a detector server and a separator server already running:
SED_MODEL_CONFIG=configs/birdcode/models/denoising_detector.yml \
uv run sed-denoising-server --host 0.0.0.0 --port 8110
```
`sed-denoising-server` takes the same options as `sed-server` (default port `8110`).
### HTTP contract
- `GET /` β model metadata: `{labels, sample_rate, frame_rate, window_duration}`
- `GET /health` β `{status: "ok"}` once the model is loaded
- `GET /labels` β ordered label list
- `POST /run` β frame-level inference; response `{predictions, shape [batch, time, classes], frame_rate}`
- `POST /run_as_classifier` β clip-level pooled inference; response shape `[batch, classes]`
- `POST /separate_and_detect` β denoising server only; per-stem audio + predictions
## Evaluation β `sed-eval`
Serve a model, then run `sed-eval` against it with an **eval config** (*what* to evaluate) and an **http-client config** (*how* to reach the model β a `url` plus optional `timeout`/`retries`/`auth`; the client kind is auto-detected from the server).
```bash
# write an http-client config pointing at the running server, e.g.:
# url: http://HOST:8100
uv run sed-eval --eval-config configs/birdcode/frame_eval.yml \
--httpclient-config configs/birdcode/httpclient.yml \
[--checkpoint-dir <dir>] [--output-dir <dir>]
```
- `--checkpoint-dir` β resumable checkpoint directory (auto-generated under `checkpoints/sed/` if omitted).
- `--output-dir` β override the eval config's `output_dir`.
- `sed-eval --resume <checkpoint-dir>` β resume a run; configs are reloaded from the checkpoint.
### Eval configs
| Config | Pathway | Datasets | Sample rate |
|---|---|---|---|
| `configs/birdcode/frame_eval.yml` | frame (detection) | 68 WABAD sites + Powdermill + XC-AJ | 32 kHz |
| `configs/birdcode/birdset_clip_eval.yml` | clip (classification) | 8 BirdSet test splits | 32 kHz |
An eval config selects the pathway through its dataset lists: `frame_datasets` (strong labels, with `species_column`) go through detection; `clip_datasets` (weak labels) through classification.
### Metrics
- **Frame pathway**: frame mAP, event mAP per IoU threshold, thresholded precision/recall/F1.
- **Clip pathway**: cmAP (headline), cmAP5, mAP, pcmAP, MultilabelAUROC, top-1/top-3 accuracy, per-class AP, and `gt_coverage`.
### Results
Each eval run writes `<output_dir>/results.yaml`, updated after every dataset:
- `model` β the served model's metadata (`GET /` response)
- `frame_eval` β the scoring parameters used
- `frame_datasets.<name>` β per-dataset detection metrics
- `clip_datasets.<name>` β per-dataset classification metrics
## Large-scale inference (LSI)
Run a served detector over a dataset, persist per-recording results as compressed `.npz` shards, then postprocess (and optionally enrich) them into selection tables. Three stages: **run β postprocess β features**. Each stage takes `--job-index N --num-jobs M` to split the work across an array of parallel jobs, and writes a `lineage.yaml` chaining back to the stage that produced its input.
The LSI configs (`configs/inference/lsi_birdcode_*.yml`) run the BirdCODE frame detector over the full Xeno-Canto and iNaturalist training splits; they read their datasets from `configs/data/inference/`.
### Run β `sed-lsi`
Builds a dataset from a **run config** (*what* to run) and a detector client from an **http-client config** (*how* to reach the model), then runs the sharded engine over this job's slice.
```bash
# with the appropriate server running (see below):
uv run sed-lsi --run-config configs/inference/lsi_birdcode_xc.yml \
--httpclient-config <httpclient.yml> [--job-index N --num-jobs M] [--output-dir DIR]
```
The run config's `output.detail` selects what is stored per recording β and which server the `url` must reach:
| `detail` | Stored | Server |
|---|---|---|
| `preds` | combined framewise predictions | a `sed-server` detector server |
| `denoised` | predictions + a threshold-gated denoised waveform | a `sed-denoising-server` server |
| `stems` | the above + every separated stem (audio + preds) | a `sed-denoising-server` server |
### Postprocess β `sed-lsi-postprocess`
Reads the combined predictions in each shard and writes a per-recording selection table (1:1 with the input shards). Re-postprocessing is a cheap re-run into a sibling directory.
```bash
uv run sed-lsi-postprocess --config configs/inference/lsi_birdcode_xc_postprocess.yml \
--run-dir <run_dir> [--job-index N --num-jobs M]
```
`--run-dir` overrides the config's `input.run_dir` (postprocess several runs
with one config).
#### Geography filtering
Setting `postprocessing.geo_filter: true` drops detections for species whose range maps exclude a recording's location (using the latitude/longitude stored in each shard). It requires `postprocessing.range_map_dir` β a directory (local path or cloud URI) of `*.gpkg` range-map files, globbed at startup and checked to exist before any shards are processed:
```yaml
postprocessing:
geo_filter: true
range_map_dir: geography/range_maps # dir of *.gpkg range maps
```
To use geography filtering, download the open range-map dataset from iNaturalist (<https://www.inaturalist.org/pages/range_maps>) into `range_map_dir`. Each range map's species `name` is resolved to a GBIF canonical name to match the detector's labels. The filter fails open: a detection is dropped only on positive out-of-range evidence (valid coordinates **and** a range map that excludes the point); recordings without coordinates, or species without a range map, are left untouched.
### Features β `sed-lsi-features`
Enriches a postprocessed selection table with per-event `v0minimal` acoustic features. Writes enriched selection tables 1:1 with the postprocess shards.
```bash
uv run sed-lsi-features --config configs/inference/lsi_birdcode_xc_features.yml \
--run-dir <run_dir> --postprocessing postprocessed_thr0.50_merge1.00_nms0.80_geo \
[--job-index N --num-jobs M]
```
## Loading a dataset with attached selection tables (Python)
Public GCS buckets hold BirdCODE detections as selection tables for a subset of **Xeno-Canto** and **iNaturalist** recordings. Two data configs load each corpus with those tables attached via the `attach_lsi_selection_tables` transform:
- `configs/data/inference/xeno_canto_selection_tables.yml`
- `configs/data/inference/inaturalist_selection_tables.yml`
Load either with `alp_data.dataset_from_config`, importing the transforms module first so the custom transform is registered:
```python
import io
import pandas as pd
from alp_data import dataset_from_config
import sound_event_detection.data.transforms # noqa: F401 β registers attach_lsi_selection_tables
dataset, meta = dataset_from_config("configs/data/inference/xeno_canto_selection_tables.yml")
print(meta["attach_lsi_selection_tables"]) # {'matched': ..., 'unmatched': ...}
# The attached `selection_table` column lives on the metadata backend
# (`dataset._data`), so you can read it without decoding audio. It is a TSV
# string (empty for unmatched rows); parse it into a DataFrame of events:
for row in dataset._data:
if row["selection_table"]:
events = pd.read_csv(io.StringIO(row["selection_table"]), sep="\t")
break
```
Each row of a parsed `selection_table` is one detection event, with columns:
- `Begin Time (s)`, `End Time (s)` β the event's span within the recording
- `Species` β predicted class label
- `Score` β mean BirdCODE probability over the event
- 13 `v0minimal` acoustic-feature columns (see `sound_event_detection.inference.features_v0minimal.FEATURE_COLS`)
|