Modular pipeline for detecting and reviewing Wild Turkey vocalizations in ARU (autonomous recording unit) audio. Runs BirdNET on WAV recordings, extracts candidate clips, and provides a Streamlit app for labeling and inter-rater adjudication.
The labeling stage produces a reviewed dataset of time–frequency regions on each 3-second candidate clip, each region tagged Tom or Hen. Clips with no turkey are saved with an empty region list (with an optional "other birds present" flag). BirdNET performs the candidate detection; a turkey-specific frame-level model trained on the reviewed regions then localizes the exact timing, duration, and sex of each call.
Place raw audio under data/ARU_<id>/ inside your project root. WAV files must follow the naming convention <device_id>_YYYYMMDD_HHMMSS.wav (e.g. 2MA09358_20260304_051500.wav). Files that don't match are quarantined rather than crashing the pipeline.
All generated outputs are written under data/_outputs/ and are excluded from version control.
project-root/
├── data/
│ ├── ARU_01/ ← raw audio (read-only, not committed)
│ │ └── *.wav
│ └── _outputs/ ← generated (not committed)
│ ├── runs/<run_id>/
│ └── review/
Requirements: Git for Windows, Anaconda or Miniconda.
-
Clone the repository:
git clone https://github.com/ZackLoken/turkey-audio-detection.git cd turkey-audio-detection -
Create and activate the conda environment, which installs the package along with CUDA PyTorch, TensorFlow and ffmpeg:
conda env create -f gobbler.yml conda activate gobbler
Put audio in data/ARU_01/ (or data/ARU_02/, etc.) under your project root, then run the full pipeline:
python -m turkey_audio_detection.cli run-all --project-root "C:\path\to\your\project"
Processing thousands of files takes a while. Add --prime-window-only to restrict BirdNET to the window around sunrise when turkeys are most vocal (30 min before to 3 hr after, by default), which cuts total run time substantially:
python -m turkey_audio_detection.cli run-all --project-root "C:\path\to\your\project" --prime-window-only
The pipeline prints a run_id (e.g. run_20260424T205153Z) when it finishes, note it, whoever reviews the clips will need it. TensorFlow and BirdNET print verbose startup messages to the console; these are normal. A progress bar shows per-file status while BirdNET is running.
Individual stages can also be run one at a time; see Stage-by-stage CLI usage.
Troubleshooting: if birdnetlib fails to import, confirm ffmpeg is on your PATH: conda install -c conda-forge ffmpeg
Once you have the project's data\ folder and a run_id from whoever ran BirdNET, launch the app:
python -m turkey_audio_detection.app
In the sidebar, set Project root to the folder containing that data\ folder and press Enter. Run ID then becomes a dropdown listing every run found under data\_outputs\runs\; pick the one you were given. Add a reviewer name, then for each clip draw rectangles on the spectrogram around any Tom or Hen calls (switch the active-label radio between Tom and Hen as needed), tick Other birds present / Unsure when relevant, and click Save & Next. See the Review app section below for details.
Troubleshooting: if audio playback is silent, check that your WAV files are readable: python -c "import soundfile; print(soundfile.info('yourfile.wav'))"
All outputs are written under data/_outputs/runs/<run_id>/.
# Index recordings and compute sunrise windows
python -m turkey_audio_detection.cli index-data --project-root .
# Run BirdNET on indexed files (use run_id from previous step)
python -m turkey_audio_detection.cli run-birdnet --project-root . --run-id <run_id>
# Extract 3-second review clips for Wild Turkey detections
python -m turkey_audio_detection.cli extract-clips --project-root . --run-id <run_id>
# Or run all three stages at once
python -m turkey_audio_detection.cli run-all --project-root .
# Compute inter-rater Cohen's kappa from reviewer label files
python -m turkey_audio_detection.cli adjudicate --project-root .
python -m turkey_audio_detection.app
- Sidebar: set project root, select run ID, enter reviewer name. The collapsible "How to label" expander lives below.
- Main panel: custom audio control and a mel spectrogram pinned to 50–14000 Hz with labeled time and frequency axes. The vertical black bar over the spectrogram tracks the audio playhead.
- Region annotation:
- Toggle the active label (
Tom= lime green,Hen= royal blue) between drawings to label both call types on one clip - Drag rectangles on the spectrogram around each call; the rectangle's x-extent encodes time, y-extent encodes frequency. Each box is auto-previewed (audio bandpass-filtered to the box's frequency bounds) the moment you finish drawing
- Click an existing rectangle to replay its band-limited audio; double-click to delete it
- Tick
Other birds presentwhen any non-turkey bird is audible in the clip - Tick
Unsurewhen you can't reliably tell whether a turkey is in the clip; these rows are excluded from agreement stats by default
- Toggle the active label (
Save & Nextwrites one row todata/_outputs/review/labels/<reviewer_id>.csvand advances. Saving on an empty canvas creates an explicit "no turkey" label.Previousrevisits a labeled clip (regions re-render so you can edit them).Reset canvasclears drawings and removes any saved snapshot for the clip so it becomes unlabeled again.Jump to first unlabeledseeks to the next clip without a saved snapshot. Go to # jumps to a typed detection number, so a queue can be split into ranges across annotators.- Each CSV row contains:
item_id, detection_id, reviewer_id, reviewer_name, regions_json, other_birds_present, unsure, tom_present, hen_present, label_timestamp_utc, session_id.regions_jsonis a JSON list of{start_s, end_s, freq_min_hz, freq_max_hz, label}objects;tom_present/hen_presentare denormalized for cheap filtering. - Run
adjudicateafter two reviewers finish to get pairwise Cohen's kappa per attribute (tom_presentandhen_present) and a disagreements export tagged by attribute.
Once reviewers have produced labeled clips, train the frame-level sound-event-detection (SED) model, evaluate it, and run it over full recordings. Training and inference run in PyTorch on CUDA; the first run downloads the BirdSet ConvNeXt weights (~390 MB) to the Hugging Face cache.
First, copy site_map.example.csv to data/site_map.csv and fill in one aru_id,site_id row per ARU so train/val/test split by site (unmapped ARUs fall back to one-site-per-ARU). data/ is gitignored, so your populated map stays local and is never overwritten by updates.
# Train on one or more runs' labels. Aggregates per-reviewer CSVs via majority vote,
# splits train/val/test grouped by SITE to avoid leakage,
# then fine-tunes a BirdSet-pretrained ConvNeXt frontend with a BiGRU temporal head,
# gradually unfreezing backbone stages across phases (scaling LR/batch, early stopping).
python -m turkey_audio_detection.cli train --project-root . --run-id <run_id> [--run-id <run_id> ...]
# Evaluate on the held-out test split: per-class event precision/recall/F1 with
# time-axis IoU matching (swept over several IoU thresholds) + segment F1.
python -m turkey_audio_detection.cli evaluate --project-root . --model-id <model_id> --run-id <run_id>
# Optuna hyperparameter search (resumable when given a --storage sqlite path).
python -m turkey_audio_detection.cli hpo --project-root . --run-id <run_id> --n-trials 30 --storage hpo.db
# Run the trained model over full recordings (sliding window). Returns
# per-call events (start_s, end_s, sex, score) and presence/counts per site/day.
python -m turkey_audio_detection.cli classify --project-root . --model-id <model_id> --audio-glob "data/ARU_*/**/*.wav"
data/_outputs/models/<model_id>/checkpoint.pt: best-validation model state + config + mel params + per-class thresholdsdata/_outputs/models/<model_id>/train_metrics.csv: per-phase/epoch loss + frame-level precision/recall/F1data/_outputs/models/<model_id>/splits.csv: train/val/test assignment withsite_iddata/_outputs/models/<model_id>/eval.csv: event-level metrics (IoU sweep) + segment F1data/_outputs/inference/<inference_id>/events/<source_filename>.csv: per-call events(start_time_s, end_time_s, sex, score)data/_outputs/inference/<inference_id>/aggregate_counts.csv: presence + call counts per site/day/sex
- Input: 3-second clip → torchaudio log-mel (128 mels), matched to the BirdSet checkpoint's preprocessing so the pretrained weights stay valid
- Backbone:
DBD-research-group/ConvNeXT-Base-BirdSet-XCL(bird-pretrained); the first two stages are tapped (~80 ms/frame) and the deeper stages dropped; gradually unfrozen during fine-tuning - Head: collapse frequency → BiGRU (or TCN) temporal head → per-frame Tom/Hen sigmoid logits
- Loss: per-frame focal (or BCE) on time-projected box targets; rejected candidates (no turkey) become all-zero negatives
- Inference: the trained model slides over the full recording → average overlapping per-frame probs → per-class threshold → group frames into events → counts
- Augmentation: SpecAugment + linear-power Mixup / background-mix
Splits & aggregation: train groups train/val/test by site (leave-site-out). By default only consensus clips are used; pass --include-non-consensus to include disagreement-flagged clips.
