26 |
27 | This repo contains BirdNET scripts for processing large amounts of audio data or single audio files.
28 | This is the most advanced version of BirdNET for acoustic analyses and we will keep this repository up-to-date with new models and improved interfaces to enable scientists with no CS background to run the analysis.
29 |
30 | Feel free to use BirdNET for your acoustic analyses and research.
31 | If you do, please cite as:
32 |
33 | ```bibtex
34 | @article{kahl2021birdnet,
35 | title={BirdNET: A deep learning solution for avian diversity monitoring},
36 | author={Kahl, Stefan and Wood, Connor M and Eibl, Maximilian and Klinck, Holger},
37 | journal={Ecological Informatics},
38 | volume={61},
39 | pages={101236},
40 | year={2021},
41 | publisher={Elsevier}
42 | }
43 | ```
44 |
45 | ## Documentation
46 |
47 | You can access documentation for this project [here](https://birdnet-team.github.io/BirdNET-Analyzer/).
48 |
49 | ## Download
50 |
51 | You can download installers for Windows and macOS from the [releases page](https://github.com/birdnet-team/BirdNET-Analyzer/releases/latest).
52 | Models can be found on [Zenodo](https://zenodo.org/records/15050749).
53 |
54 | ## About
55 |
56 | Developed by the [K. Lisa Yang Center for Conservation Bioacoustics](https://www.birds.cornell.edu/ccb/) at the [Cornell Lab of Ornithology](https://www.birds.cornell.edu/home) in collaboration with [Chemnitz University of Technology](https://www.tu-chemnitz.de/index.html.en).
57 |
58 | Go to https://birdnet.cornell.edu to learn more about the project.
59 |
60 | Want to use BirdNET to analyze a large dataset? Don't hesitate to contact us: ccb-birdnet@cornell.edu
61 |
62 | **Have a question, remark, or feature request? Please start a new issue thread to let us know. Feel free to submit a pull request.**
63 |
64 | ## License
65 |
66 | - **Source Code**: The source code for this project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
67 | - **Models**: The models used in this project are licensed under the [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/).
68 |
69 | Please ensure you review and adhere to the specific license terms provided with each model.
70 |
71 | *Please note that all educational and research purposes are considered non-commercial use and it is therefore freely permitted to use BirdNET models in any way.*
72 |
73 | ## Funding
74 |
75 | This project is supported by Jake Holshuh (Cornell class of ´69) and The Arthur Vining Davis Foundations.
76 | Our work in the K. Lisa Yang Center for Conservation Bioacoustics is made possible by the generosity of K. Lisa Yang to advance innovative conservation technologies to inspire and inform the conservation of wildlife and habitats.
77 |
78 | The development of BirdNET is supported by the German Federal Ministry of Education and Research through the project “BirdNET+” (FKZ 01|S22072).
79 | The German Federal Ministry for the Environment, Nature Conservation and Nuclear Safety contributes through the “DeepBirdDetect” project (FKZ 67KI31040E).
80 | In addition, the Deutsche Bundesstiftung Umwelt supports BirdNET through the project “RangerSound” (project 39263/01).
81 |
82 | ## Partners
83 |
84 | BirdNET is a joint effort of partners from academia and industry.
85 | Without these partnerships, this project would not have been possible.
86 | Thank you!
87 |
88 | 
89 |
--------------------------------------------------------------------------------
/birdnet_analyzer/__init__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.analyze import analyze
2 | from birdnet_analyzer.embeddings import embeddings
3 | from birdnet_analyzer.search import search
4 | from birdnet_analyzer.segments import segments
5 | from birdnet_analyzer.species import species
6 | from birdnet_analyzer.train import train
7 |
8 | __version__ = "2.0.0"
9 | __all__ = ["analyze", "embeddings", "search", "segments", "species", "train"]
10 |
--------------------------------------------------------------------------------
/birdnet_analyzer/analyze/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import birdnet_analyzer.config as cfg
4 | from birdnet_analyzer.analyze.core import analyze
5 |
6 | POSSIBLE_ADDITIONAL_COLUMNS_MAP = {
7 | "lat": lambda: cfg.LATITUDE,
8 | "lon": lambda: cfg.LONGITUDE,
9 | "week": lambda: cfg.WEEK,
10 | "overlap": lambda: cfg.SIG_OVERLAP,
11 | "sensitivity": lambda: cfg.SIGMOID_SENSITIVITY,
12 | "min_conf": lambda: cfg.MIN_CONFIDENCE,
13 | "species_list": lambda: cfg.SPECIES_LIST_FILE or "",
14 | "model": lambda: os.path.basename(cfg.MODEL_PATH),
15 | }
16 |
17 | __all__ = [
18 | "analyze",
19 | ]
20 |
--------------------------------------------------------------------------------
/birdnet_analyzer/analyze/__main__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.analyze.cli import main
2 |
3 | main()
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/analyze/cli.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer import analyze
2 | from birdnet_analyzer.utils import runtime_error_handler
3 |
4 |
5 | @runtime_error_handler
6 | def main():
7 | import os
8 | from multiprocessing import freeze_support
9 |
10 | from birdnet_analyzer import cli
11 |
12 | # Freeze support for executable
13 | freeze_support()
14 |
15 | parser = cli.analyzer_parser()
16 |
17 | args = parser.parse_args()
18 |
19 | try:
20 | if os.get_terminal_size().columns >= 64:
21 | print(cli.ASCII_LOGO, flush=True)
22 | except Exception:
23 | pass
24 |
25 | if "additional_columns" in args and args.additional_columns and "csv" not in args.rtype:
26 | import warnings
27 |
28 | warnings.warn("The --additional_columns argument is only valid for CSV output. It will be ignored.", stacklevel=1)
29 |
30 | analyze(**vars(args))
31 |
--------------------------------------------------------------------------------
/birdnet_analyzer/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
4 |
5 | #################
6 | # Misc settings #
7 | #################
8 |
9 | # Random seed for gaussian noise
10 | RANDOM_SEED: int = 42
11 |
12 | ##########################
13 | # Model paths and config #
14 | ##########################
15 |
16 | MODEL_VERSION: str = "V2.4"
17 | PB_MODEL: str = os.path.join(SCRIPT_DIR, "checkpoints/V2.4/BirdNET_GLOBAL_6K_V2.4_Model")
18 | # MODEL_PATH = PB_MODEL # This will load the protobuf model
19 | MODEL_PATH: str = os.path.join(SCRIPT_DIR, "checkpoints/V2.4/BirdNET_GLOBAL_6K_V2.4_Model_FP32.tflite")
20 | MDATA_MODEL_PATH: str = os.path.join(SCRIPT_DIR, "checkpoints/V2.4/BirdNET_GLOBAL_6K_V2.4_MData_Model_V2_FP16.tflite")
21 | LABELS_FILE: str = os.path.join(SCRIPT_DIR, "checkpoints/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels.txt")
22 | TRANSLATED_LABELS_PATH: str = os.path.join(SCRIPT_DIR, "labels/V2.4")
23 |
24 | ##################
25 | # Audio settings #
26 | ##################
27 |
28 | # We use a sample rate of 48kHz, so the model input size is
29 | # (batch size, 48000 kHz * 3 seconds) = (1, 144000)
30 | # Recordings will be resampled automatically.
31 | SAMPLE_RATE: int = 48000
32 |
33 | # We're using 3-second chunks
34 | SIG_LENGTH: float = 3.0
35 |
36 | # Define overlap between consecutive chunks <3.0; 0 = no overlap
37 | SIG_OVERLAP: float = 0
38 |
39 | # Define minimum length of audio chunk for prediction,
40 | # chunks shorter than 3 seconds will be padded with zeros
41 | SIG_MINLEN: float = 1.0
42 |
43 | # Frequency range. This is model specific and should not be changed.
44 | SIG_FMIN: int = 0
45 | SIG_FMAX: int = 15000
46 |
47 | # Settings for bandpass filter
48 | BANDPASS_FMIN: int = 0
49 | BANDPASS_FMAX: int = 15000
50 |
51 | # Top N species to display in selection table, ignored if set to None
52 | TOP_N = None
53 |
54 | # Audio speed
55 | AUDIO_SPEED: float = 1.0
56 |
57 | #####################
58 | # Metadata settings #
59 | #####################
60 |
61 | LATITUDE: float = -1
62 | LONGITUDE: float = -1
63 | WEEK: int = -1
64 | LOCATION_FILTER_THRESHOLD: float = 0.03
65 |
66 | ######################
67 | # Inference settings #
68 | ######################
69 |
70 | # If None or empty file, no custom species list will be used
71 | # Note: Entries in this list have to match entries from the LABELS_FILE
72 | # We use the 2024 eBird taxonomy for species names (Clements list)
73 | CODES_FILE: str = os.path.join(SCRIPT_DIR, "eBird_taxonomy_codes_2024E.json")
74 | SPECIES_LIST_FILE: str = os.path.join(SCRIPT_DIR, "example/species_list.txt")
75 |
76 | # Supported file types
77 | ALLOWED_FILETYPES: list[str] = ["wav", "flac", "mp3", "ogg", "m4a", "wma", "aiff", "aif"]
78 |
79 | # Number of threads to use for inference.
80 | # Can be as high as number of CPUs in your system
81 | CPU_THREADS: int = 8
82 | TFLITE_THREADS: int = 1
83 |
84 | # False will output logits, True will convert to sigmoid activations
85 | APPLY_SIGMOID: bool = True
86 | SIGMOID_SENSITIVITY: float = 1.0
87 |
88 | # Minimum confidence score to include in selection table
89 | # (be aware: if APPLY_SIGMOID = False, this no longer represents
90 | # probabilities and needs to be adjusted)
91 | MIN_CONFIDENCE: float = 0.25
92 |
93 | # Number of consecutive detections for one species to merge into one
94 | # If set to 1 or 0, no merging will be done
95 | # If set to None, all detections will be included
96 | MERGE_CONSECUTIVE: int = 1
97 |
98 | # Number of samples to process at the same time. Higher values can increase
99 | # processing speed, but will also increase memory usage.
100 | # Might only be useful for GPU inference.
101 | BATCH_SIZE: int = 1
102 |
103 |
104 | # Number of seconds to load from a file at a time
105 | # Files will be loaded into memory in segments that are only as long as this value
106 | # Lowering this value results in lower memory usage
107 | FILE_SPLITTING_DURATION: int = 600
108 |
109 | # Whether to use noise to pad the signal
110 | # If set to False, the signal will be padded with zeros
111 | USE_NOISE: bool = False
112 |
113 | # Specifies the output format. 'table' denotes a Raven selection table,
114 | # 'audacity' denotes a TXT file with the same format as Audacity timeline labels
115 | # 'csv' denotes a generic CSV file with start, end, species and confidence.
116 | RESULT_TYPES: set[str] | list[str] = {"table"}
117 | ADDITIONAL_COLUMNS: list[str] | None = None
118 | OUTPUT_RAVEN_FILENAME: str = "BirdNET_SelectionTable.txt" # this is for combined Raven selection tables only
119 | # OUTPUT_RTABLE_FILENAME: str = "BirdNET_RTable.csv"
120 | OUTPUT_KALEIDOSCOPE_FILENAME: str = "BirdNET_Kaleidoscope.csv"
121 | OUTPUT_CSV_FILENAME: str = "BirdNET_CombinedTable.csv"
122 |
123 | # File name of the settings csv for batch analysis
124 | ANALYSIS_PARAMS_FILENAME: str = "BirdNET_analysis_params.csv"
125 |
126 | # Whether to skip existing results in the output path
127 | # If set to False, existing files will not be overwritten
128 | SKIP_EXISTING_RESULTS: bool = False
129 |
130 | COMBINE_RESULTS: bool = False
131 | #####################
132 | # Training settings #
133 | #####################
134 |
135 | # Sample crop mode
136 | SAMPLE_CROP_MODE: str = "center"
137 |
138 | # List of non-event classes
139 | NON_EVENT_CLASSES: list[str] = ["noise", "other", "background", "silence"]
140 |
141 | # Upsampling settings
142 | UPSAMPLING_RATIO: float = 0.0
143 | UPSAMPLING_MODE = "repeat"
144 |
145 | # Number of epochs to train for
146 | TRAIN_EPOCHS: int = 50
147 |
148 | # Batch size for training
149 | TRAIN_BATCH_SIZE: int = 32
150 |
151 | # Validation split (percentage)
152 | TRAIN_VAL_SPLIT: float = 0.2
153 |
154 | # Learning rate for training
155 | TRAIN_LEARNING_RATE: float = 0.0001
156 |
157 | # Number of hidden units in custom classifier
158 | # If >0, a two-layer classifier will be trained
159 | TRAIN_HIDDEN_UNITS: int = 0
160 |
161 | # Dropout rate for training
162 | TRAIN_DROPOUT: float = 0.0
163 |
164 | # Whether to use mixup for training
165 | TRAIN_WITH_MIXUP: bool = False
166 |
167 | # Whether to apply label smoothing for training
168 | TRAIN_WITH_LABEL_SMOOTHING: bool = False
169 |
170 | # Whether to use focal loss for training
171 | TRAIN_WITH_FOCAL_LOSS: bool = False
172 |
173 | # Focal loss gamma parameter
174 | FOCAL_LOSS_GAMMA: float = 2.0
175 |
176 | # Focal loss alpha parameter
177 | FOCAL_LOSS_ALPHA: float = 0.25
178 |
179 | # Model output format
180 | TRAINED_MODEL_OUTPUT_FORMAT: str = "tflite"
181 |
182 | # Model save mode (replace or append new classifier)
183 | TRAINED_MODEL_SAVE_MODE: str = "replace"
184 |
185 | # Cache settings
186 | TRAIN_CACHE_MODE: str | None = None
187 | TRAIN_CACHE_FILE: str = "train_cache.npz"
188 |
189 | # Use automatic Hyperparameter tuning
190 | AUTOTUNE: bool = False
191 |
192 | # How many trials are done for the hyperparameter tuning
193 | AUTOTUNE_TRIALS: int = 50
194 |
195 | # How many executions per trial are done for the hyperparameter tuning
196 | # Mutliple executions will be averaged, so the evaluation is more consistent
197 | AUTOTUNE_EXECUTIONS_PER_TRIAL: int = 1
198 |
199 | # If a binary classification model is trained.
200 | # This value will be detected automatically in the training script, if only one class and a non-event class is used.
201 | BINARY_CLASSIFICATION: bool = False
202 |
203 | # If a model for a multi-label setting is trained.
204 | # This value will automatically be set, if subfolders in the input direcotry are named with multiple classes separated by commas.
205 | MULTI_LABEL: bool = False
206 |
207 | ################
208 | # Runtime vars #
209 | ################
210 |
211 | # File input path and output path for selection tables
212 | INPUT_PATH: str = ""
213 | OUTPUT_PATH: str = ""
214 |
215 | # Training data path
216 | TRAIN_DATA_PATH: str = ""
217 | TEST_DATA_PATH: str = ""
218 |
219 | CODES = {}
220 | LABELS: list[str] = []
221 | TRANSLATED_LABELS: list[str] = []
222 | SPECIES_LIST: list[str] = []
223 | ERROR_LOG_FILE: str = os.path.join(SCRIPT_DIR, "error_log.txt")
224 | FILE_LIST = []
225 | FILE_STORAGE_PATH: str = ""
226 |
227 | # Path to custom trained classifier
228 | # If None, no custom classifier will be used
229 | # Make sure to set the LABELS_FILE above accordingly
230 | CUSTOM_CLASSIFIER: str | None = None
231 |
232 | ######################
233 | # Get and set config #
234 | ######################
235 |
236 |
237 | def get_config():
238 | return {k: v for k, v in globals().items() if k.isupper()}
239 |
240 |
241 | def set_config(c: dict):
242 | for k, v in c.items():
243 | globals()[k] = v
244 |
--------------------------------------------------------------------------------
/birdnet_analyzer/embeddings/__init__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.embeddings.core import embeddings
2 |
3 | __all__ = ["embeddings"]
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/embeddings/__main__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.embeddings.cli import main
2 |
3 | main()
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/embeddings/cli.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer import embeddings
2 | from birdnet_analyzer.utils import runtime_error_handler
3 |
4 |
5 | @runtime_error_handler
6 | def main():
7 | from birdnet_analyzer import cli
8 |
9 | parser = cli.embeddings_parser()
10 | args = parser.parse_args()
11 |
12 | embeddings(**vars(args))
13 |
--------------------------------------------------------------------------------
/birdnet_analyzer/embeddings/core.py:
--------------------------------------------------------------------------------
1 | def embeddings(
2 | audio_input: str,
3 | database: str,
4 | *,
5 | overlap: float = 0.0,
6 | audio_speed: float = 1.0,
7 | fmin: int = 0,
8 | fmax: int = 15000,
9 | threads: int = 8,
10 | batch_size: int = 1,
11 | file_output: str | None = None,
12 | ):
13 | """
14 | Generates embeddings for audio files using the BirdNET-Analyzer.
15 | This function processes audio files to extract embeddings, which are
16 | representations of audio features. The embeddings can be used for
17 | further analysis or comparison.
18 | Args:
19 | audio_input (str): Path to the input audio file or directory containing audio files.
20 | database (str): Path to the database where embeddings will be stored.
21 | overlap (float, optional): Overlap between consecutive audio segments in seconds. Defaults to 0.0.
22 | audio_speed (float, optional): Speed factor for audio processing. Defaults to 1.0.
23 | fmin (int, optional): Minimum frequency (in Hz) for audio analysis. Defaults to 0.
24 | fmax (int, optional): Maximum frequency (in Hz) for audio analysis. Defaults to 15000.
25 | threads (int, optional): Number of threads to use for processing. Defaults to 8.
26 | batch_size (int, optional): Number of audio segments to process in a single batch. Defaults to 1.
27 | Raises:
28 | FileNotFoundError: If the input path or database path does not exist.
29 | ValueError: If any of the parameters are invalid.
30 | Note:
31 | Ensure that the required model files are downloaded and available before
32 | calling this function. The `ensure_model_exists` function is used to
33 | verify this.
34 | Example:
35 | embeddings(
36 | "path/to/audio",
37 | "path/to/database",
38 | overlap=0.5,
39 | audio_speed=1.0,
40 | fmin=500,
41 | fmax=10000,
42 | threads=4,
43 | batch_size=2
44 | )
45 | """
46 | from birdnet_analyzer.embeddings.utils import run
47 | from birdnet_analyzer.utils import ensure_model_exists
48 |
49 | ensure_model_exists()
50 | run(audio_input, database, overlap, audio_speed, fmin, fmax, threads, batch_size, file_output)
51 |
52 |
53 | def get_database(db_path: str):
54 | """Get the database object. Creates or opens the databse.
55 | Args:
56 | db: The path to the database.
57 | Returns:
58 | The database object.
59 | """
60 | import os
61 |
62 | from perch_hoplite.db import sqlite_usearch_impl
63 |
64 | if not os.path.exists(db_path):
65 | os.makedirs(os.path.dirname(db_path), exist_ok=True)
66 | return sqlite_usearch_impl.SQLiteUsearchDB.create(
67 | db_path=db_path,
68 | usearch_cfg=sqlite_usearch_impl.get_default_usearch_config(embedding_dim=1024), # TODO: dont hardcode this
69 | )
70 | return sqlite_usearch_impl.SQLiteUsearchDB.create(db_path=db_path)
71 |
--------------------------------------------------------------------------------
/birdnet_analyzer/embeddings/utils.py:
--------------------------------------------------------------------------------
1 | """Module used to extract embeddings for samples."""
2 |
3 | import datetime
4 | import os
5 | from functools import partial
6 | from multiprocessing import Pool
7 |
8 | import numpy as np
9 | from ml_collections import ConfigDict
10 | from perch_hoplite.db import interface as hoplite
11 | from perch_hoplite.db import sqlite_usearch_impl
12 | from tqdm import tqdm
13 |
14 | import birdnet_analyzer.config as cfg
15 | from birdnet_analyzer import audio, model, utils
16 | from birdnet_analyzer.analyze.utils import get_raw_audio_from_file
17 | from birdnet_analyzer.embeddings.core import get_database
18 |
19 | DATASET_NAME: str = "birdnet_analyzer_dataset"
20 |
21 |
22 | def analyze_file(item, db: sqlite_usearch_impl.SQLiteUsearchDB):
23 | """Extracts the embeddings for a file.
24 |
25 | Args:
26 | item: (filepath, config)
27 | """
28 |
29 | # Get file path and restore cfg
30 | fpath: str = item[0]
31 | cfg.set_config(item[1])
32 |
33 | offset = 0
34 | duration = cfg.FILE_SPLITTING_DURATION
35 |
36 | try:
37 | fileLengthSeconds = int(audio.get_audio_file_length(fpath))
38 | except Exception as ex:
39 | # Write error log
40 | print(f"Error: Cannot analyze audio file {fpath}. File corrupt?\n", flush=True)
41 | utils.write_error_log(ex)
42 |
43 | return
44 |
45 | # Start time
46 | start_time = datetime.datetime.now()
47 |
48 | # Status
49 | print(f"Analyzing {fpath}", flush=True)
50 |
51 | source_id = fpath
52 |
53 | # Process each chunk
54 | try:
55 | while offset < fileLengthSeconds:
56 | chunks = get_raw_audio_from_file(fpath, offset, duration)
57 | start, end = offset, cfg.SIG_LENGTH + offset
58 | samples = []
59 | timestamps = []
60 |
61 | for c in range(len(chunks)):
62 | # Add to batch
63 | samples.append(chunks[c])
64 | timestamps.append([start, end])
65 |
66 | # Advance start and end
67 | start += cfg.SIG_LENGTH - cfg.SIG_OVERLAP
68 | end = start + cfg.SIG_LENGTH
69 |
70 | # Check if batch is full or last chunk
71 | if len(samples) < cfg.BATCH_SIZE and c < len(chunks) - 1:
72 | continue
73 |
74 | # Prepare sample and pass through model
75 | data = np.array(samples, dtype="float32")
76 | e = model.embeddings(data)
77 |
78 | # Add to results
79 | for i in range(len(samples)):
80 | # Get timestamp
81 | s_start, s_end = timestamps[i]
82 |
83 | # Check if embedding already exists
84 | existing_embedding = db.get_embeddings_by_source(DATASET_NAME, source_id, np.array([s_start, s_end]))
85 |
86 | if existing_embedding.size == 0:
87 | # Get prediction
88 | embeddings = e[i]
89 |
90 | # Store embeddings
91 | embeddings_source = hoplite.EmbeddingSource(DATASET_NAME, source_id, np.array([s_start, s_end]))
92 |
93 | # Insert into database
94 | db.insert_embedding(embeddings, embeddings_source)
95 | db.commit()
96 |
97 | # Reset batch
98 | samples = []
99 | timestamps = []
100 |
101 | offset = offset + duration
102 |
103 | except Exception as ex:
104 | # Write error log
105 | print(f"Error: Cannot analyze audio file {fpath}.", flush=True)
106 | utils.write_error_log(ex)
107 |
108 | return
109 |
110 | delta_time = (datetime.datetime.now() - start_time).total_seconds()
111 | print(f"Finished {fpath} in {delta_time:.2f} seconds", flush=True)
112 |
113 |
114 | def check_database_settings(db: sqlite_usearch_impl.SQLiteUsearchDB):
115 | try:
116 | settings = db.get_metadata("birdnet_analyzer_settings")
117 | if settings["BANDPASS_FMIN"] != cfg.BANDPASS_FMIN or settings["BANDPASS_FMAX"] != cfg.BANDPASS_FMAX or settings["AUDIO_SPEED"] != cfg.AUDIO_SPEED:
118 | raise ValueError(
119 | "Database settings do not match current configuration. DB Settings are: fmin:"
120 | + f"{settings['BANDPASS_FMIN']}, fmax: {settings['BANDPASS_FMAX']}, audio_speed: {settings['AUDIO_SPEED']}"
121 | )
122 | except KeyError:
123 | settings = ConfigDict({"BANDPASS_FMIN": cfg.BANDPASS_FMIN, "BANDPASS_FMAX": cfg.BANDPASS_FMAX, "AUDIO_SPEED": cfg.AUDIO_SPEED})
124 | db.insert_metadata("birdnet_analyzer_settings", settings)
125 | db.commit()
126 |
127 |
128 | def create_file_output(output_path: str, db: sqlite_usearch_impl.SQLiteUsearchDB):
129 | """Creates a file output for the database.
130 |
131 | Args:
132 | output_path: Path to the output file.
133 | db: Database object.
134 | """
135 | # Check if output path exists
136 | if not os.path.exists(output_path):
137 | os.makedirs(output_path)
138 | # Get all embeddings
139 | embedding_ids = db.get_embedding_ids()
140 |
141 | # Write embeddings to file
142 | for embedding_id in embedding_ids:
143 | embedding = db.get_embedding(embedding_id)
144 | source = db.get_embedding_source(embedding_id)
145 |
146 | # Get start and end time
147 | start, end = source.offsets
148 |
149 | source_id = source.source_id.rsplit(".", 1)[0]
150 |
151 | filename = f"{source_id}_{start}_{end}.birdnet.embeddings.txt"
152 |
153 | # Get the common prefix between the output path and the filename
154 | common_prefix = os.path.commonpath([output_path, os.path.dirname(filename)])
155 | relative_filename = os.path.relpath(filename, common_prefix)
156 | target_path = os.path.join(output_path, relative_filename)
157 |
158 | # Ensure the target directory exists
159 | os.makedirs(os.path.dirname(target_path), exist_ok=True)
160 |
161 | # Write embedding values to a text file
162 | with open(target_path, "w") as f:
163 | f.write(",".join(map(str, embedding.tolist())))
164 |
165 | def run(audio_input, database, overlap, audio_speed, fmin, fmax, threads, batchsize, file_output):
166 | ### Make sure to comment out appropriately if you are not using args. ###
167 |
168 | # Set input and output path
169 | cfg.INPUT_PATH = audio_input
170 |
171 | # Parse input files
172 | if os.path.isdir(cfg.INPUT_PATH):
173 | cfg.FILE_LIST = utils.collect_audio_files(cfg.INPUT_PATH)
174 | else:
175 | cfg.FILE_LIST = [cfg.INPUT_PATH]
176 |
177 | # Set overlap
178 | cfg.SIG_OVERLAP = max(0.0, min(2.9, float(overlap)))
179 |
180 | # Set audio speed
181 | cfg.AUDIO_SPEED = max(0.01, audio_speed)
182 |
183 | # Set bandpass frequency range
184 | cfg.BANDPASS_FMIN = max(0, min(cfg.SIG_FMAX, int(fmin)))
185 | cfg.BANDPASS_FMAX = max(cfg.SIG_FMIN, min(cfg.SIG_FMAX, int(fmax)))
186 |
187 | # Set number of threads
188 | if os.path.isdir(cfg.INPUT_PATH):
189 | cfg.CPU_THREADS = max(1, int(threads))
190 | cfg.TFLITE_THREADS = 1
191 | else:
192 | cfg.CPU_THREADS = 1
193 | cfg.TFLITE_THREADS = max(1, int(threads))
194 |
195 | cfg.CPU_THREADS = 1 # TODO: with the current implementation, we can't use more than 1 thread
196 |
197 | # Set batch size
198 | cfg.BATCH_SIZE = max(1, int(batchsize))
199 |
200 | # Add config items to each file list entry.
201 | # We have to do this for Windows which does not
202 | # support fork() and thus each process has to
203 | # have its own config. USE LINUX!
204 | flist = [(f, cfg.get_config()) for f in cfg.FILE_LIST]
205 |
206 | db = get_database(database)
207 | check_database_settings(db)
208 |
209 | # Analyze files
210 | if cfg.CPU_THREADS < 2:
211 | for entry in tqdm(flist):
212 | analyze_file(entry, db)
213 | else:
214 | with Pool(cfg.CPU_THREADS) as p:
215 | tqdm(p.imap(partial(analyze_file, db=db), flist))
216 |
217 | if file_output:
218 | create_file_output(file_output, db)
219 |
220 | db.db.close()
221 |
--------------------------------------------------------------------------------
/birdnet_analyzer/evaluation/__main__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.evaluation import main
2 |
3 | main()
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/evaluation/assessment/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/evaluation/assessment/__init__.py
--------------------------------------------------------------------------------
/birdnet_analyzer/evaluation/preprocessing/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/evaluation/preprocessing/__init__.py
--------------------------------------------------------------------------------
/birdnet_analyzer/evaluation/preprocessing/utils.py:
--------------------------------------------------------------------------------
1 | """
2 | Utility Functions for Data Processing Tasks
3 |
4 | This module provides helper functions to handle common data processing tasks, such as:
5 | - Extracting recording filenames from file paths or filenames.
6 | - Reading and concatenating text files from a specified directory.
7 |
8 | It is designed to work seamlessly with pandas and file system operations.
9 | """
10 |
11 | import os
12 |
13 | import pandas as pd
14 |
15 |
16 | def extract_recording_filename(path_column: pd.Series) -> pd.Series:
17 | """
18 | Extract the recording filename from a path column.
19 |
20 | This function processes a pandas Series containing file paths and extracts the base filename
21 | (without the extension) for each path.
22 |
23 | Args:
24 | path_column (pd.Series): A pandas Series containing file paths.
25 |
26 | Returns:
27 | pd.Series: A pandas Series containing the extracted recording filenames.
28 | """
29 | # Apply a lambda function to extract the base filename without extension
30 | return path_column.apply(lambda x: os.path.splitext(os.path.basename(x))[0] if isinstance(x, str) else x)
31 |
32 |
33 | def extract_recording_filename_from_filename(filename_series: pd.Series) -> pd.Series:
34 | """
35 | Extract the recording filename from a filename Series.
36 |
37 | This function processes a pandas Series containing filenames and extracts the base filename
38 | (without the extension) for each.
39 |
40 | Args:
41 | filename_series (pd.Series): A pandas Series containing filenames.
42 |
43 | Returns:
44 | pd.Series: A pandas Series containing the extracted recording filenames.
45 | """
46 | # Apply a lambda function to split filenames and remove the extension
47 | return filename_series.apply(lambda x: x.split(".")[0] if isinstance(x, str) else x)
48 |
49 |
50 | def read_and_concatenate_files_in_directory(directory_path: str) -> pd.DataFrame:
51 | """
52 | Read and concatenate all .txt files in a directory into a single DataFrame.
53 |
54 | This function scans the specified directory for all .txt files, reads each file into a DataFrame,
55 | appends a 'source_file' column containing the filename, and concatenates all DataFrames into one.
56 | If the files have inconsistent columns, a ValueError is raised.
57 |
58 | Args:
59 | directory_path (str): Path to the directory containing the .txt files.
60 |
61 | Returns:
62 | pd.DataFrame: A concatenated DataFrame containing the data from all .txt files,
63 | or an empty DataFrame if no files are found.
64 |
65 | Raises:
66 | ValueError: If the columns in the files are inconsistent.
67 | """
68 | df_list: list[pd.DataFrame] = [] # List to hold individual DataFrames
69 | columns_set = None # To ensure consistency in column names
70 |
71 | # Iterate through each file in the directory
72 | for filename in sorted(os.listdir(directory_path)):
73 | if filename.endswith(".txt"):
74 | filepath = os.path.join(directory_path, filename) # Construct the full file path
75 |
76 | try:
77 | # Attempt to read the file as a tab-separated values file with UTF-8 encoding
78 | df = pd.read_csv(filepath, sep="\t", encoding="utf-8")
79 | except UnicodeDecodeError:
80 | # Fallback to 'latin-1' encoding if UTF-8 fails
81 | df = pd.read_csv(filepath, sep="\t", encoding="latin-1")
82 |
83 | # Check for column consistency across files
84 | if columns_set is None:
85 | columns_set = set(df.columns) # Initialize with the first file's columns
86 | elif set(df.columns) != columns_set:
87 | raise ValueError(f"File {filename} has different columns than the previous files.")
88 |
89 | # Add a column to indicate the source file for traceability
90 | df["source_file"] = filename
91 |
92 | # Append the DataFrame to the list
93 | df_list.append(df)
94 |
95 | # Concatenate all DataFrames if any were processed, else return an empty DataFrame
96 | if df_list:
97 | return pd.concat(df_list, ignore_index=True)
98 | return pd.DataFrame() # Return an empty DataFrame if no .txt files were found
99 |
--------------------------------------------------------------------------------
/birdnet_analyzer/example/soundscape.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/example/soundscape.wav
--------------------------------------------------------------------------------
/birdnet_analyzer/example/species_list.txt:
--------------------------------------------------------------------------------
1 | Accipiter cooperii_Cooper's Hawk
2 | Agelaius phoeniceus_Red-winged Blackbird
3 | Anas platyrhynchos_Mallard
4 | Anas rubripes_American Black Duck
5 | Ardea herodias_Great Blue Heron
6 | Baeolophus bicolor_Tufted Titmouse
7 | Branta canadensis_Canada Goose
8 | Bucephala albeola_Bufflehead
9 | Bucephala clangula_Common Goldeneye
10 | Buteo jamaicensis_Red-tailed Hawk
11 | Cardinalis cardinalis_Northern Cardinal
12 | Certhia americana_Brown Creeper
13 | Colaptes auratus_Northern Flicker
14 | Columba livia_Rock Pigeon
15 | Corvus brachyrhynchos_American Crow
16 | Corvus corax_Common Raven
17 | Cyanocitta cristata_Blue Jay
18 | Cygnus olor_Mute Swan
19 | Dryobates pubescens_Downy Woodpecker
20 | Dryobates villosus_Hairy Woodpecker
21 | Dryocopus pileatus_Pileated Woodpecker
22 | Eremophila alpestris_Horned Lark
23 | Haemorhous mexicanus_House Finch
24 | Haemorhous purpureus_Purple Finch
25 | Haliaeetus leucocephalus_Bald Eagle
26 | Junco hyemalis_Dark-eyed Junco
27 | Larus argentatus_Herring Gull
28 | Larus delawarensis_Ring-billed Gull
29 | Lophodytes cucullatus_Hooded Merganser
30 | Melanerpes carolinus_Red-bellied Woodpecker
31 | Meleagris gallopavo_Wild Turkey
32 | Melospiza melodia_Song Sparrow
33 | Mergus merganser_Common Merganser
34 | Mergus serrator_Red-breasted Merganser
35 | Passer domesticus_House Sparrow
36 | Poecile atricapillus_Black-capped Chickadee
37 | Regulus satrapa_Golden-crowned Kinglet
38 | Sialia sialis_Eastern Bluebird
39 | Sitta canadensis_Red-breasted Nuthatch
40 | Sitta carolinensis_White-breasted Nuthatch
41 | Spinus pinus_Pine Siskin
42 | Spinus tristis_American Goldfinch
43 | Spizelloides arborea_American Tree Sparrow
44 | Sturnus vulgaris_European Starling
45 | Thryothorus ludovicianus_Carolina Wren
46 | Turdus migratorius_American Robin
47 | Zenaida macroura_Mourning Dove
48 | Zonotrichia albicollis_White-throated Sparrow
49 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/__init__.py:
--------------------------------------------------------------------------------
1 | def main():
2 | import birdnet_analyzer.gui.multi_file as mfa
3 | import birdnet_analyzer.gui.segments as gs
4 | import birdnet_analyzer.gui.single_file as sfa
5 | import birdnet_analyzer.gui.utils as gu
6 | from birdnet_analyzer.gui import embeddings, evaluation, review, species, train
7 |
8 | gu.open_window(
9 | [
10 | sfa.build_single_analysis_tab,
11 | mfa.build_multi_analysis_tab,
12 | train.build_train_tab,
13 | gs.build_segments_tab,
14 | review.build_review_tab,
15 | species.build_species_tab,
16 | embeddings.build_embeddings_tab,
17 | evaluation.build_evaluation_tab,
18 | ]
19 | )
20 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/__main__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.gui import main
2 |
3 | main()
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/analysis.py:
--------------------------------------------------------------------------------
1 | import concurrent.futures
2 | import os
3 | from pathlib import Path
4 |
5 | import gradio as gr
6 |
7 | import birdnet_analyzer.config as cfg
8 | import birdnet_analyzer.gui.utils as gu
9 | from birdnet_analyzer import model
10 | from birdnet_analyzer.analyze.utils import (
11 | analyze_file,
12 | combine_results,
13 | save_analysis_params,
14 | )
15 |
16 | SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
17 | ORIGINAL_LABELS_FILE = str(Path(SCRIPT_DIR).parent / cfg.LABELS_FILE)
18 |
19 |
20 | def analyze_file_wrapper(entry):
21 | """
22 | Wrapper function for analyzing a file.
23 |
24 | Args:
25 | entry (tuple): A tuple where the first element is the file path and the
26 | remaining elements are arguments to be passed to the
27 | analyze.analyzeFile function.
28 |
29 | Returns:
30 | tuple: A tuple where the first element is the file path and the second
31 | element is the result of the analyze.analyzeFile function.
32 | """
33 | return (entry[0], analyze_file(entry))
34 |
35 |
36 | def run_analysis(
37 | input_path: str,
38 | output_path: str | None,
39 | use_top_n: bool,
40 | top_n: int,
41 | confidence: float,
42 | sensitivity: float,
43 | overlap: float,
44 | merge_consecutive: int,
45 | audio_speed: float,
46 | fmin: int,
47 | fmax: int,
48 | species_list_choice: str,
49 | species_list_file,
50 | lat: float,
51 | lon: float,
52 | week: int,
53 | use_yearlong: bool,
54 | sf_thresh: float,
55 | custom_classifier_file,
56 | output_types: str,
57 | additional_columns: list[str] | None,
58 | combine_tables: bool,
59 | locale: str,
60 | batch_size: int,
61 | threads: int,
62 | input_dir: str,
63 | skip_existing: bool,
64 | save_params: bool,
65 | progress: gr.Progress | None,
66 | ):
67 | """Starts the analysis.
68 |
69 | Args:
70 | input_path: Either a file or directory.
71 | output_path: The output path for the result, if None the input_path is used
72 | confidence: The selected minimum confidence.
73 | sensitivity: The selected sensitivity.
74 | overlap: The selected segment overlap.
75 | merge_consecutive: The number of consecutive segments to merge into one.
76 | audio_speed: The selected audio speed.
77 | fmin: The selected minimum bandpass frequency.
78 | fmax: The selected maximum bandpass frequency.
79 | species_list_choice: The choice for the species list.
80 | species_list_file: The selected custom species list file.
81 | lat: The selected latitude.
82 | lon: The selected longitude.
83 | week: The selected week of the year.
84 | use_yearlong: Use yearlong instead of week.
85 | sf_thresh: The threshold for the predicted species list.
86 | custom_classifier_file: Custom classifier to be used.
87 | output_type: The type of result to be generated.
88 | additional_columns: Additional columns to be added to the result.
89 | output_filename: The filename for the combined output.
90 | locale: The translation to be used.
91 | batch_size: The number of samples in a batch.
92 | threads: The number of threads to be used.
93 | input_dir: The input directory.
94 | progress: The gradio progress bar.
95 | """
96 | import birdnet_analyzer.gui.localization as loc
97 |
98 | if progress is not None:
99 | progress(0, desc=f"{loc.localize('progress-preparing')} ...")
100 |
101 | from birdnet_analyzer.analyze.core import _set_params
102 |
103 | locale = locale.lower()
104 | custom_classifier = custom_classifier_file if species_list_choice == gu._CUSTOM_CLASSIFIER else None
105 | slist = species_list_file if species_list_choice == gu._CUSTOM_SPECIES else None
106 | lat = lat if species_list_choice == gu._PREDICT_SPECIES else -1
107 | lon = lon if species_list_choice == gu._PREDICT_SPECIES else -1
108 | week = -1 if use_yearlong else week
109 |
110 | flist = _set_params(
111 | audio_input=input_dir if input_dir else input_path,
112 | min_conf=confidence,
113 | custom_classifier=custom_classifier,
114 | sensitivity=min(1.25, max(0.75, float(sensitivity))),
115 | locale=locale,
116 | overlap=max(0.0, min(2.9, float(overlap))),
117 | merge_consecutive=max(1, int(merge_consecutive)),
118 | audio_speed=max(0.1, 1.0 / (audio_speed * -1)) if audio_speed < 0 else max(1.0, float(audio_speed)),
119 | fmin=max(0, min(cfg.SIG_FMAX, int(fmin))),
120 | fmax=max(cfg.SIG_FMIN, min(cfg.SIG_FMAX, int(fmax))),
121 | bs=max(1, int(batch_size)),
122 | combine_results=combine_tables,
123 | rtype=output_types,
124 | skip_existing_results=skip_existing,
125 | threads=max(1, int(threads)),
126 | labels_file=ORIGINAL_LABELS_FILE,
127 | sf_thresh=sf_thresh,
128 | lat=lat,
129 | lon=lon,
130 | week=week,
131 | slist=slist,
132 | top_n=top_n if use_top_n else None,
133 | output=output_path,
134 | additional_columns=additional_columns,
135 | )
136 |
137 | if species_list_choice == gu._CUSTOM_CLASSIFIER:
138 | if custom_classifier_file is None:
139 | raise gr.Error(loc.localize("validation-no-custom-classifier-selected"))
140 |
141 | model.reset_custom_classifier()
142 |
143 | gu.validate(cfg.FILE_LIST, loc.localize("validation-no-audio-files-found"))
144 |
145 | result_list = []
146 |
147 | if progress is not None:
148 | progress(0, desc=f"{loc.localize('progress-starting')} ...")
149 |
150 | # Analyze files
151 | if cfg.CPU_THREADS < 2:
152 | result_list.extend(analyze_file_wrapper(entry) for entry in flist)
153 | else:
154 | with concurrent.futures.ProcessPoolExecutor(max_workers=cfg.CPU_THREADS) as executor:
155 | futures = (executor.submit(analyze_file_wrapper, arg) for arg in flist)
156 | for i, f in enumerate(concurrent.futures.as_completed(futures), start=1):
157 | if progress is not None:
158 | progress((i, len(flist)), total=len(flist), unit="files")
159 | result = f.result()
160 |
161 | result_list.append(result)
162 |
163 | # Combine results?
164 | if cfg.COMBINE_RESULTS:
165 | combine_list = [[r[1] for r in result_list if r[0] == i[0]][0] for i in flist]
166 | print(f"Combining results, writing to {cfg.OUTPUT_PATH}...", end="", flush=True)
167 | combine_results(combine_list)
168 | print("done!", flush=True)
169 |
170 | if save_params:
171 | save_analysis_params(os.path.join(cfg.OUTPUT_PATH, cfg.ANALYSIS_PARAMS_FILENAME))
172 |
173 | return (
174 | [[os.path.relpath(r[0], input_dir), bool(r[1])] for r in result_list]
175 | if input_dir
176 | else result_list[0][1]["csv"]
177 | if result_list[0][1]
178 | else None
179 | )
180 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/assets/arrow_down.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/assets/arrow_left.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/assets/arrow_right.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/assets/arrow_up.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/assets/gui.css:
--------------------------------------------------------------------------------
1 | footer {
2 | display: none !important;
3 | }
4 |
5 | #single_file_audio,
6 | #single_file_audio * {
7 | max-height: 81.6px;
8 | min-height: 0;
9 | }
10 |
11 | #update-available a {
12 | text-decoration: none;
13 | }
14 |
15 | :root {
16 | --block-title-text-color: var(--neutral-800);
17 | --block-info-text-color: var(--neutral-500);
18 | }
19 |
20 | #single-file-output td:first-of-type span {
21 | text-align: center;
22 | }
23 |
24 | #embeddings-search-results {
25 | max-height: 1107px;
26 | overflow: auto;
27 | flex-wrap: nowrap;
28 | padding-right: 5px;
29 | }
30 |
31 | #heart {
32 | display: inline;
33 | background-color: white;
34 | border-radius: 3px;
35 | margin-right: 3px;
36 | padding: 1px;
37 | }
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/assets/gui.js:
--------------------------------------------------------------------------------
1 | function init() {
2 | function checkForNewerVersion() {
3 | let gui_version_element = document.getElementById("current-version")
4 |
5 | if (gui_version_element && gui_version_element.textContent != "main") {
6 | console.log("Checking for newer version...");
7 |
8 | function sendGetRequest(url) {
9 | return new Promise((resolve, reject) => {
10 | const xhr = new XMLHttpRequest();
11 | xhr.open("GET", url);
12 | xhr.onload = () => {
13 | if (xhr.status === 200) {
14 | resolve(xhr.responseText);
15 | } else {
16 | reject(new Error(`Request failed with status ${xhr.status}`));
17 | }
18 | };
19 | xhr.onerror = () => {
20 | reject(new Error("Request failed"));
21 | };
22 | xhr.send();
23 | });
24 | }
25 |
26 | const apiUrl = "https://api.github.com/repos/birdnet-team/BirdNET-Analyzer/releases/latest";
27 |
28 | sendGetRequest(apiUrl)
29 | .then(response => {
30 | const current_version = document.getElementById("current-version").textContent;
31 | const response_object = JSON.parse(response);
32 | const latest_version = response_object.tag_name;
33 |
34 | if (latest_version.startsWith("v")) {
35 | latest_version = latest_version.slice(1);
36 | }
37 |
38 | if (current_version !== latest_version) {
39 | const updateNotification = document.getElementById("update-available");
40 |
41 | updateNotification.style.display = "block";
42 | const linkElement = updateNotification.getElementsByTagName("a")[0]
43 | linkElement.href = response_object.html_url;
44 | linkElement.target = "_blank";
45 | }
46 | })
47 | .catch(error => {
48 | console.error(error);
49 | });
50 | }
51 | }
52 |
53 | function overwriteStyles() {
54 | console.log("Overwriting styles...");
55 | const styles = document.createElement("style");
56 | styles.innerHTML = "@media (width <= 1024px) { .app {max-width: initial !important;}}";
57 | document.head.appendChild(styles);
58 | }
59 |
60 | function bindReviewKeyShortcuts() {
61 | const posBtn = document.getElementById("positive-button");
62 | const negBtn = document.getElementById("negative-button");
63 | const skipBtn = document.getElementById("skip-button");
64 | const undoBtn = document.getElementById("undo-button");
65 |
66 | if (!posBtn || !negBtn) return;
67 |
68 | console.log("Binding review key shortcuts...");
69 |
70 | document.addEventListener("keydown", function (event) {
71 | const reviewTabBtn = document.getElementById("review-tab-button");
72 |
73 | if (reviewTabBtn.ariaSelected === "false") return;
74 |
75 | if (event.key === "ArrowUp") {
76 | event.preventDefault();
77 | posBtn.click();
78 | } else if (event.key === "ArrowDown") {
79 | event.preventDefault();
80 | negBtn.click();
81 | } else if (event.key === "ArrowLeft") {
82 | event.preventDefault();
83 | undoBtn.click();
84 | } else if (event.key === "ArrowRight") {
85 | event.preventDefault();
86 | skipBtn.click();
87 | }
88 | });
89 | }
90 |
91 | checkForNewerVersion();
92 | overwriteStyles();
93 | bindReviewKeyShortcuts();
94 | }
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/assets/img/birdnet-icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/gui/assets/img/birdnet-icon.ico
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/assets/img/birdnet_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/gui/assets/img/birdnet_logo.png
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/assets/img/birdnet_logo_no_transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/birdnet_analyzer/gui/assets/img/birdnet_logo_no_transparent.png
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/assets/img/clo-logo-bird.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/localization.py:
--------------------------------------------------------------------------------
1 | # ruff: noqa: PLW0603
2 | import json
3 | import os
4 |
5 | from birdnet_analyzer.gui import settings
6 |
7 | SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
8 | LANGUAGE_DIR = os.path.join(os.path.dirname(SCRIPT_DIR), "lang")
9 | LANGUAGE_LOOKUP = {}
10 | TARGET_LANGUAGE = settings.FALLBACK_LANGUAGE
11 |
12 |
13 | def load_local_state():
14 | """
15 | Loads the local language settings and populates the LANGUAGE_LOOKUP dictionary with the appropriate translations.
16 | This function performs the following steps:
17 | """
18 | global LANGUAGE_LOOKUP
19 | global TARGET_LANGUAGE
20 |
21 | settings.ensure_settings_file()
22 |
23 | try:
24 | with open(settings.GUI_SETTINGS_PATH, encoding="utf-8") as f:
25 | settings_data = json.load(f)
26 |
27 | if "language-id" in settings_data:
28 | TARGET_LANGUAGE = settings_data["language-id"]
29 | except FileNotFoundError:
30 | print(f"gui-settings.json not found. Using fallback language {settings.FALLBACK_LANGUAGE}.")
31 |
32 | try:
33 | with open(f"{LANGUAGE_DIR}/{TARGET_LANGUAGE}.json", encoding="utf-8") as f:
34 | LANGUAGE_LOOKUP = json.load(f)
35 | except FileNotFoundError:
36 | print(
37 | f"Language file for {TARGET_LANGUAGE} not found in {LANGUAGE_DIR}."
38 | + "Using fallback language {settings.FALLBACK_LANGUAGE}."
39 | )
40 |
41 | if TARGET_LANGUAGE != settings.FALLBACK_LANGUAGE:
42 | with open(f"{LANGUAGE_DIR}/{settings.FALLBACK_LANGUAGE}.json") as f:
43 | fallback: dict = json.load(f)
44 |
45 | for key, value in fallback.items():
46 | if key not in LANGUAGE_LOOKUP:
47 | LANGUAGE_LOOKUP[key] = value
48 |
49 |
50 | def localize(key: str) -> str:
51 | """
52 | Translates a given key into its corresponding localized string.
53 |
54 | Args:
55 | key (str): The key to be localized.
56 |
57 | Returns:
58 | str: The localized string corresponding to the given key.
59 | If the key is not found in the localization lookup, the original key is returned.
60 | """
61 | return LANGUAGE_LOOKUP.get(key, key)
62 |
63 |
64 | def set_language(language: str):
65 | """
66 | Sets the language for the application by updating the GUI settings file.
67 | This function ensures that the settings file exists, reads the current settings,
68 | updates the "language-id" field with the provided language, and writes the updated
69 | settings back to the file.
70 |
71 | Args:
72 | language (str): The language identifier to set in the settings file.
73 | """
74 | if language:
75 | settings.set_setting("language-id", language)
76 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/segments.py:
--------------------------------------------------------------------------------
1 | import concurrent.futures
2 | import os
3 | from functools import partial
4 |
5 | import gradio as gr
6 |
7 | import birdnet_analyzer.config as cfg
8 | import birdnet_analyzer.gui.localization as loc
9 | import birdnet_analyzer.gui.utils as gu
10 | from birdnet_analyzer.segments.utils import extract_segments
11 |
12 |
13 | def extract_segments_wrapper(entry):
14 | return (entry[0][0], extract_segments(entry))
15 |
16 |
17 | @gu.gui_runtime_error_handler
18 | def _extract_segments(
19 | audio_dir, result_dir, output_dir, min_conf, num_seq, audio_speed, seq_length, threads, progress=gr.Progress()
20 | ):
21 | from birdnet_analyzer.segments.utils import parse_files, parse_folders
22 |
23 | gu.validate(audio_dir, loc.localize("validation-no-audio-directory-selected"))
24 |
25 | if not result_dir:
26 | result_dir = audio_dir
27 |
28 | if not output_dir:
29 | output_dir = audio_dir
30 |
31 | if progress is not None:
32 | progress(0, desc=f"{loc.localize('progress-search')} ...")
33 |
34 | # Parse audio and result folders
35 | cfg.FILE_LIST = parse_folders(audio_dir, result_dir)
36 |
37 | # Set output folder
38 | cfg.OUTPUT_PATH = output_dir
39 |
40 | # Set number of threads
41 | cfg.CPU_THREADS = int(threads)
42 |
43 | # Set confidence threshold
44 | cfg.MIN_CONFIDENCE = max(0.01, min(0.99, min_conf))
45 |
46 | # Parse file list and make list of segments
47 | cfg.FILE_LIST = parse_files(cfg.FILE_LIST, max(1, int(num_seq)))
48 |
49 | # Audio speed
50 | cfg.AUDIO_SPEED = max(0.1, 1.0 / (audio_speed * -1)) if audio_speed < 0 else max(1.0, float(audio_speed))
51 |
52 | # Add config items to each file list entry.
53 | # We have to do this for Windows which does not
54 | # support fork() and thus each process has to
55 | # have its own config. USE LINUX!
56 | # flist = [(entry, max(cfg.SIG_LENGTH, float(seq_length)), cfg.getConfig()) for entry in cfg.FILE_LIST]
57 | flist = [(entry, float(seq_length), cfg.get_config()) for entry in cfg.FILE_LIST]
58 |
59 | result_list = []
60 |
61 | # Extract segments
62 | if cfg.CPU_THREADS < 2:
63 | for i, entry in enumerate(flist):
64 | result = extract_segments_wrapper(entry)
65 | result_list.append(result)
66 |
67 | if progress is not None:
68 | progress((i, len(flist)), total=len(flist), unit="files")
69 | else:
70 | with concurrent.futures.ProcessPoolExecutor(max_workers=cfg.CPU_THREADS) as executor:
71 | futures = (executor.submit(extract_segments_wrapper, arg) for arg in flist)
72 | for i, f in enumerate(concurrent.futures.as_completed(futures), start=1):
73 | if progress is not None:
74 | progress((i, len(flist)), total=len(flist), unit="files")
75 | result = f.result()
76 |
77 | result_list.append(result)
78 |
79 | return [[os.path.relpath(r[0], audio_dir), r[1]] for r in result_list]
80 |
81 |
82 | def build_segments_tab():
83 | with gr.Tab(loc.localize("segments-tab-title")):
84 | audio_directory_state = gr.State()
85 | result_directory_state = gr.State()
86 | output_directory_state = gr.State()
87 |
88 | def select_directory_to_state_and_tb(state_key):
89 | return (gu.select_directory(collect_files=False, state_key=state_key),) * 2
90 |
91 | with gr.Row():
92 | select_audio_directory_btn = gr.Button(
93 | loc.localize("segments-tab-select-audio-input-directory-button-label")
94 | )
95 | selected_audio_directory_tb = gr.Textbox(show_label=False, interactive=False)
96 | select_audio_directory_btn.click(
97 | partial(select_directory_to_state_and_tb, state_key="segments-audio-dir"),
98 | outputs=[selected_audio_directory_tb, audio_directory_state],
99 | show_progress=False,
100 | )
101 |
102 | with gr.Row():
103 | select_result_directory_btn = gr.Button(
104 | loc.localize("segments-tab-select-results-input-directory-button-label")
105 | )
106 | selected_result_directory_tb = gr.Textbox(
107 | show_label=False,
108 | interactive=False,
109 | placeholder=loc.localize("segments-tab-results-input-textbox-placeholder"),
110 | )
111 | select_result_directory_btn.click(
112 | partial(select_directory_to_state_and_tb, state_key="segments-result-dir"),
113 | outputs=[result_directory_state, selected_result_directory_tb],
114 | show_progress=False,
115 | )
116 |
117 | with gr.Row():
118 | select_output_directory_btn = gr.Button(loc.localize("segments-tab-output-selection-button-label"))
119 | selected_output_directory_tb = gr.Textbox(
120 | show_label=False,
121 | interactive=False,
122 | placeholder=loc.localize("segments-tab-output-selection-textbox-placeholder"),
123 | )
124 | select_output_directory_btn.click(
125 | partial(select_directory_to_state_and_tb, state_key="segments-output-dir"),
126 | outputs=[selected_output_directory_tb, output_directory_state],
127 | show_progress=False,
128 | )
129 |
130 | min_conf_slider = gr.Slider(
131 | minimum=0.1,
132 | maximum=0.99,
133 | step=0.01,
134 | value=cfg.MIN_CONFIDENCE,
135 | label=loc.localize("segments-tab-min-confidence-slider-label"),
136 | info=loc.localize("segments-tab-min-confidence-slider-info"),
137 | )
138 | num_seq_number = gr.Number(
139 | 100,
140 | label=loc.localize("segments-tab-max-seq-number-label"),
141 | info=loc.localize("segments-tab-max-seq-number-info"),
142 | minimum=1,
143 | )
144 | audio_speed_slider = gr.Slider(
145 | minimum=-10,
146 | maximum=10,
147 | value=cfg.AUDIO_SPEED,
148 | step=1,
149 | label=loc.localize("inference-settings-audio-speed-slider-label"),
150 | info=loc.localize("inference-settings-audio-speed-slider-info"),
151 | )
152 | seq_length_number = gr.Number(
153 | cfg.SIG_LENGTH,
154 | label=loc.localize("segments-tab-seq-length-number-label"),
155 | info=loc.localize("segments-tab-seq-length-number-info"),
156 | minimum=0.1,
157 | )
158 | threads_number = gr.Number(
159 | 4,
160 | label=loc.localize("segments-tab-threads-number-label"),
161 | info=loc.localize("segments-tab-threads-number-info"),
162 | minimum=1,
163 | )
164 |
165 | extract_segments_btn = gr.Button(loc.localize("segments-tab-extract-button-label"), variant="huggingface")
166 |
167 | result_grid = gr.Matrix(
168 | headers=[
169 | loc.localize("segments-tab-result-dataframe-column-file-header"),
170 | loc.localize("segments-tab-result-dataframe-column-execution-header"),
171 | ],
172 | )
173 |
174 | extract_segments_btn.click(
175 | _extract_segments,
176 | inputs=[
177 | audio_directory_state,
178 | result_directory_state,
179 | output_directory_state,
180 | min_conf_slider,
181 | num_seq_number,
182 | audio_speed_slider,
183 | seq_length_number,
184 | threads_number,
185 | ],
186 | outputs=result_grid,
187 | )
188 |
189 |
190 | if __name__ == "__main__":
191 | gu.open_window(build_segments_tab)
192 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/settings.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import sys
4 | from pathlib import Path
5 |
6 | import birdnet_analyzer.config as cfg
7 | from birdnet_analyzer import utils
8 |
9 | if utils.FROZEN:
10 | # divert stdout & stderr to logs.txt file since we have no console when deployed
11 | userdir = Path.home()
12 |
13 | if sys.platform == "win32":
14 | userdir /= "AppData/Roaming"
15 | elif sys.platform == "linux":
16 | userdir /= ".local/share"
17 | elif sys.platform == "darwin":
18 | userdir /= "Library/Application Support"
19 |
20 | APPDIR = userdir / "BirdNET-Analyzer-GUI"
21 |
22 | APPDIR.mkdir(parents=True, exist_ok=True)
23 |
24 | sys.stderr = sys.stdout = open(str(APPDIR / "logs.txt"), "a") # noqa: SIM115
25 | cfg.ERROR_LOG_FILE = str(APPDIR / os.path.basename(cfg.ERROR_LOG_FILE))
26 | else:
27 | APPDIR = ""
28 |
29 | FALLBACK_LANGUAGE = "en"
30 | SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
31 | GUI_SETTINGS_PATH = os.path.join(APPDIR if utils.FROZEN else os.path.dirname(SCRIPT_DIR), "gui-settings.json")
32 | LANG_DIR = str(Path(SCRIPT_DIR).parent / "lang")
33 | STATE_SETTINGS_PATH = os.path.join(APPDIR if utils.FROZEN else os.path.dirname(SCRIPT_DIR), "state.json")
34 |
35 |
36 | def get_state_dict() -> dict:
37 | """
38 | Retrieves the state dictionary from a JSON file specified by STATE_SETTINGS_PATH.
39 |
40 | If the file does not exist, it creates an empty JSON file and returns an empty dictionary.
41 | If any other exception occurs during file operations, it logs the error and returns an empty dictionary.
42 |
43 | Returns:
44 | dict: The state dictionary loaded from the JSON file, or an empty dictionary if the file does not exist or an error occurs.
45 | """
46 | try:
47 | with open(STATE_SETTINGS_PATH, encoding="utf-8") as f:
48 | return json.load(f)
49 | except FileNotFoundError:
50 | try:
51 | with open(STATE_SETTINGS_PATH, "w", encoding="utf-8") as f:
52 | json.dump({}, f)
53 | return {}
54 | except Exception as e:
55 | utils.write_error_log(e)
56 | return {}
57 |
58 |
59 | def get_state(key: str, default=None):
60 | """
61 | Retrieves the value associated with the given key from the state dictionary.
62 |
63 | Args:
64 | key (str): The key to look up in the state dictionary.
65 | default: The value to return if the key is not found. Defaults to None.
66 |
67 | Returns:
68 | str: The value associated with the key if found, otherwise the default value.
69 | """
70 | return get_state_dict().get(key, default)
71 |
72 |
73 | def set_state(key: str, value: str):
74 | """
75 | Updates the state dictionary with the given key-value pair and writes it to a JSON file.
76 |
77 | Args:
78 | key (str): The key to update in the state dictionary.
79 | value (str): The value to associate with the key in the state dictionary.
80 | """
81 | try:
82 | state = get_state_dict()
83 | state[key] = value
84 |
85 | with open(STATE_SETTINGS_PATH, "w") as f:
86 | json.dump(state, f, indent=4)
87 | except Exception as e:
88 | utils.write_error_log(e)
89 |
90 |
91 | def ensure_settings_file():
92 | """
93 | Ensures that the settings file exists at the specified path. If the file does not exist,
94 | it creates a new settings file with default settings.
95 |
96 | If the file creation fails, the error is logged.
97 | """
98 | if not os.path.exists(GUI_SETTINGS_PATH):
99 | try:
100 | with open(GUI_SETTINGS_PATH, "w") as f:
101 | settings = {"language-id": FALLBACK_LANGUAGE, "theme": "light"}
102 | f.write(json.dumps(settings, indent=4))
103 | except Exception as e:
104 | utils.write_error_log(e)
105 |
106 |
107 | def get_setting(key, default=None):
108 | """
109 | Retrieves the value associated with the given key from the settings file.
110 |
111 | Args:
112 | key (str): The key to look up in the settings file.
113 | default: The value to return if the key is not found. Defaults to None.
114 |
115 | Returns:
116 | str: The value associated with the key if found, otherwise the default value.
117 | """
118 | ensure_settings_file()
119 |
120 | try:
121 | with open(GUI_SETTINGS_PATH, encoding="utf-8") as f:
122 | settings_dict: dict = json.load(f)
123 |
124 | return settings_dict.get(key, default)
125 | except FileNotFoundError:
126 | return default
127 |
128 |
129 | def set_setting(key, value):
130 | ensure_settings_file()
131 | settings_dict = {}
132 |
133 | try:
134 | with open(GUI_SETTINGS_PATH, "r+", encoding="utf-8") as f:
135 | settings_dict = json.load(f)
136 | settings_dict[key] = value
137 | f.seek(0)
138 | json.dump(settings_dict, f, indent=4)
139 | f.truncate()
140 |
141 | except FileNotFoundError:
142 | pass
143 |
144 |
145 | def theme():
146 | options = ("light", "dark")
147 | current_time = get_setting("theme", "light")
148 |
149 | return current_time if current_time in options else "light"
150 |
--------------------------------------------------------------------------------
/birdnet_analyzer/gui/species.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import gradio as gr
4 |
5 | import birdnet_analyzer.config as cfg
6 | import birdnet_analyzer.gui.localization as loc
7 | import birdnet_analyzer.gui.utils as gu
8 | from birdnet_analyzer.gui import settings
9 |
10 |
11 | @gu.gui_runtime_error_handler
12 | def run_species_list(out_path, filename, lat, lon, week, use_yearlong, sf_thresh, sortby):
13 | from birdnet_analyzer.species.utils import run
14 |
15 | gu.validate(out_path, loc.localize("validation-no-directory-selected"))
16 |
17 | run(
18 | os.path.join(out_path, filename if filename else "species_list.txt"),
19 | lat,
20 | lon,
21 | -1 if use_yearlong else week,
22 | sf_thresh,
23 | sortby,
24 | )
25 |
26 | gr.Info(f"{loc.localize('species-tab-finish-info')} {cfg.OUTPUT_PATH}")
27 |
28 |
29 | def build_species_tab():
30 | with gr.Tab(loc.localize("species-tab-title")) as species_tab:
31 | output_directory_state = gr.State()
32 | select_directory_btn = gr.Button(loc.localize("species-tab-select-output-directory-button-label"))
33 | classifier_name = gr.Textbox(
34 | "species_list.txt",
35 | visible=False,
36 | info=loc.localize("species-tab-filename-textbox-label"),
37 | )
38 |
39 | def select_directory_and_update_tb(name_tb):
40 | dir_name = gu.select_folder(state_key="species-output-dir")
41 |
42 | if dir_name:
43 | settings.set_state("species-output-dir", dir_name)
44 | return (
45 | dir_name,
46 | gr.Textbox(label=dir_name, visible=True, value=name_tb),
47 | )
48 |
49 | return None, name_tb
50 |
51 | select_directory_btn.click(
52 | select_directory_and_update_tb,
53 | inputs=classifier_name,
54 | outputs=[output_directory_state, classifier_name],
55 | show_progress=False,
56 | )
57 |
58 | lat_number, lon_number, week_number, sf_thresh_number, yearlong_checkbox, map_plot = (
59 | gu.species_list_coordinates(show_map=True)
60 | )
61 |
62 | sortby = gr.Radio(
63 | [
64 | (loc.localize("species-tab-sort-radio-option-frequency"), "freq"),
65 | (loc.localize("species-tab-sort-radio-option-alphabetically"), "alpha"),
66 | ],
67 | value="freq",
68 | label=loc.localize("species-tab-sort-radio-label"),
69 | info=loc.localize("species-tab-sort-radio-info"),
70 | )
71 |
72 | start_btn = gr.Button(loc.localize("species-tab-start-button-label"), variant="huggingface")
73 | start_btn.click(
74 | run_species_list,
75 | inputs=[
76 | output_directory_state,
77 | classifier_name,
78 | lat_number,
79 | lon_number,
80 | week_number,
81 | yearlong_checkbox,
82 | sf_thresh_number,
83 | sortby,
84 | ],
85 | )
86 |
87 | species_tab.select(
88 | lambda lat, lon: gu.plot_map_scatter_mapbox(lat, lon, zoom=3), inputs=[lat_number, lon_number], outputs=map_plot
89 | )
90 |
91 | return lat_number, lon_number, map_plot
92 |
93 |
94 | if __name__ == "__main__":
95 | gu.open_window(build_species_tab)
96 |
--------------------------------------------------------------------------------
/birdnet_analyzer/network/__init__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.network.client import send_request
2 | from birdnet_analyzer.network.server import start_server
3 |
4 | __all__ = ["send_request", "start_server"]
5 |
--------------------------------------------------------------------------------
/birdnet_analyzer/network/client.py:
--------------------------------------------------------------------------------
1 | """Client to send requests to the server."""
2 |
3 | import json
4 | import os
5 | import time
6 | from multiprocessing import freeze_support
7 |
8 | import requests
9 |
10 |
11 | def send_request(host: str, port: int, fpath: str, mdata: str) -> dict:
12 | """
13 | Sends a classification request to the server.
14 | This function sends an HTTP POST request to a server for analyzing an audio file.
15 | It includes the audio file and additional metadata in the request payload.
16 | Args:
17 | host (str): The host address of the server.
18 | port (int): The port number to connect to on the server.
19 | fpath (str): The file path of the audio file to be analyzed.
20 | mdata (str): A JSON string containing additional metadata for the analysis.
21 | dict: The JSON-decoded response from the server.
22 | Returns:
23 | dict: The JSON-decoded response from the server.
24 | Raises:
25 | FileNotFoundError: If the specified file path does not exist.
26 | requests.exceptions.RequestException: If the HTTP request fails.
27 | """
28 | url = f"http://{host}:{port}/analyze"
29 |
30 | print(f"Requesting analysis for {fpath}")
31 |
32 | with open(fpath, "rb") as f:
33 | # Make payload
34 | multipart_form_data = {"audio": (fpath.rsplit(os.sep, 1)[-1], f), "meta": (None, mdata)}
35 |
36 | # Send request
37 | start_time = time.time()
38 | response = requests.post(url, files=multipart_form_data)
39 | end_time = time.time()
40 |
41 | print(f"Response: {response.text}, Time: {end_time - start_time:.4f}s", flush=True)
42 |
43 | # Convert to dict
44 | return json.loads(response.text)
45 |
46 |
47 | def _save_result(data, fpath):
48 | """Saves the server response.
49 |
50 | Args:
51 | data: The response data.
52 | fpath: The path to save the data at.
53 | """
54 | # Make directory
55 | dir_path = os.path.dirname(fpath)
56 | os.makedirs(dir_path, exist_ok=True)
57 |
58 | # Save result
59 | with open(fpath, "w") as f:
60 | json.dump(data, f, indent=4)
61 |
62 |
63 | if __name__ == "__main__":
64 | from birdnet_analyzer import cli
65 |
66 | # Freeze support for executable
67 | freeze_support()
68 |
69 | # Parse arguments
70 | parser = cli.client_parser()
71 |
72 | args = parser.parse_args()
73 |
74 | # TODO: If specified, read and send species list
75 |
76 | # Make metadata
77 | mdata = {
78 | "lat": args.lat,
79 | "lon": args.lon,
80 | "week": args.week,
81 | "overlap": args.overlap,
82 | "sensitivity": args.sensitivity,
83 | "sf_thresh": args.sf_thresh,
84 | "pmode": args.pmode,
85 | "num_results": args.num_results,
86 | "save": args.save,
87 | }
88 |
89 | # Send request
90 | data = send_request(args.host, args.port, args.input, json.dumps(mdata))
91 |
92 | # Save result
93 | fpath = args.output if args.output else args.i.rsplit(".", 1)[0] + ".BirdNET.results.json"
94 |
95 | _save_result(data, fpath)
96 |
--------------------------------------------------------------------------------
/birdnet_analyzer/network/server.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import tempfile
4 | from multiprocessing import freeze_support
5 |
6 | import birdnet_analyzer.config as cfg
7 | from birdnet_analyzer import cli, utils
8 |
9 |
10 | def start_server(host="0.0.0.0", port=8080, spath="uploads/", threads=1, locale="en"):
11 | """
12 | Starts a web server for the BirdNET Analyzer.
13 | Args:
14 | host (str): The hostname or IP address to bind the server to. Defaults to "0.0.0.0".
15 | port (int): The port number to listen on. Defaults to 8080.
16 | spath (str): The file storage path for uploads. Defaults to "uploads/".
17 | threads (int): The number of threads to use for TensorFlow Lite inference. Defaults to 1.
18 | locale (str): The locale for translated labels. Defaults to "en".
19 | Behavior:
20 | - Ensures the required model files exist.
21 | - Loads eBird codes and labels, including translated labels if available for the specified locale.
22 | - Configures various settings such as file storage path, minimum confidence, result types, and temporary output path.
23 | - Starts a Bottle web server to handle requests.
24 | - Cleans up temporary files upon server shutdown.
25 | Note:
26 | This function blocks execution while the server is running.
27 | """
28 | import bottle
29 |
30 | import birdnet_analyzer.analyze.utils as analyze
31 |
32 | utils.ensure_model_exists()
33 |
34 | # Load eBird codes, labels
35 | cfg.CODES = analyze.load_codes()
36 | cfg.LABELS = utils.read_lines(cfg.LABELS_FILE)
37 |
38 | # Load translated labels
39 | lfile = os.path.join(
40 | cfg.TRANSLATED_LABELS_PATH, os.path.basename(cfg.LABELS_FILE).replace(".txt", f"_{locale}.txt")
41 | )
42 |
43 | if locale not in ["en"] and os.path.isfile(lfile):
44 | cfg.TRANSLATED_LABELS = utils.read_lines(lfile)
45 | else:
46 | cfg.TRANSLATED_LABELS = cfg.LABELS
47 |
48 | # Set storage file path
49 | cfg.FILE_STORAGE_PATH = spath
50 |
51 | # Set min_conf to 0.0, because we want all results
52 | cfg.MIN_CONFIDENCE = 0.0
53 |
54 | # Set path for temporary result file
55 | cfg.OUTPUT_PATH = tempfile.mkdtemp()
56 |
57 | # Set result types
58 | cfg.RESULT_TYPES = ["audacity"]
59 |
60 | # Set number of TFLite threads
61 | cfg.TFLITE_THREADS = threads
62 |
63 | # Run server
64 | print(f"UP AND RUNNING! LISTENING ON {host}:{port}", flush=True)
65 |
66 | try:
67 | bottle.run(host=host, port=port, quiet=True)
68 | finally:
69 | shutil.rmtree(cfg.OUTPUT_PATH)
70 |
71 |
72 | if __name__ == "__main__":
73 | # Freeze support for executable
74 | freeze_support()
75 |
76 | # Parse arguments
77 | parser = cli.server_parser()
78 |
79 | args = parser.parse_args()
80 |
81 | start_server(**vars(args))
82 |
--------------------------------------------------------------------------------
/birdnet_analyzer/network/utils.py:
--------------------------------------------------------------------------------
1 | """Module to create a remote endpoint for classification.
2 |
3 | Can be used to start up a server and feed it classification requests.
4 | """
5 |
6 | import json
7 | import os
8 | import tempfile
9 | from datetime import date, datetime
10 |
11 | import bottle
12 |
13 | import birdnet_analyzer.config as cfg
14 | from birdnet_analyzer import analyze, species, utils
15 |
16 |
17 | def result_pooling(lines: list[str], num_results=5, pmode="avg"):
18 | """Parses the results into list of (species, score).
19 |
20 | Args:
21 | lines: List of result scores.
22 | num_results: The number of entries to be returned.
23 | pmode: Decides how the score for each species is computed.
24 | If "max" used the maximum score for the species,
25 | if "avg" computes the average score per species.
26 |
27 | Returns:
28 | A List of (species, score).
29 | """
30 | # Parse results
31 | results = {}
32 |
33 | for line in lines:
34 | d = line.split("\t")
35 | species = d[2].replace(", ", "_")
36 | score = float(d[-1])
37 |
38 | if species not in results:
39 | results[species] = []
40 |
41 | results[species].append(score)
42 |
43 | # Compute score for each species
44 | for species in results:
45 | if pmode == "max":
46 | results[species] = max(results[species])
47 | else:
48 | results[species] = sum(results[species]) / len(results[species])
49 |
50 | # Sort results
51 | results = sorted(results.items(), key=lambda x: x[1], reverse=True)
52 |
53 | return results[:num_results]
54 |
55 |
56 | @bottle.route("/healthcheck", method="GET")
57 | def healthcheck():
58 | """Checks the health of the running server.
59 | Returns:
60 | A json message.
61 | """
62 | return json.dumps({"msg": "Server is healthy."})
63 |
64 |
65 | @bottle.route("/analyze", method="POST")
66 | def handle_request():
67 | """Handles a classification request.
68 |
69 | Takes a POST request and tries to analyze it.
70 |
71 | The response contains the result or error message.
72 |
73 | Returns:
74 | A json response with the result.
75 | """
76 | # Print divider
77 | print(f"{'#' * 20} {datetime.now()} {'#' * 20}")
78 |
79 | # Get request payload
80 | upload = bottle.request.files.get("audio")
81 | mdata = json.loads(bottle.request.forms.get("meta", {}))
82 |
83 | if not upload:
84 | return json.dumps({"msg": "No audio file."})
85 |
86 | print(mdata)
87 |
88 | # Get filename
89 | name, ext = os.path.splitext(upload.filename.lower())
90 | file_path = upload.filename
91 | file_path_tmp = None
92 |
93 | # Save file
94 | try:
95 | if ext[1:].lower() in cfg.ALLOWED_FILETYPES:
96 | if mdata.get("save", False):
97 | save_path = os.path.join(cfg.FILE_STORAGE_PATH, str(date.today()))
98 |
99 | os.makedirs(save_path, exist_ok=True)
100 |
101 | file_path = os.path.join(save_path, name + ext)
102 | else:
103 | save_path = ""
104 | file_path_tmp = tempfile.mkstemp(suffix=ext.lower(), dir=cfg.OUTPUT_PATH)
105 | file_path = file_path_tmp.name
106 |
107 | upload.save(file_path, overwrite=True)
108 | else:
109 | return json.dumps({"msg": "Filetype not supported."})
110 |
111 | except Exception as ex:
112 | if file_path_tmp:
113 | os.unlink(file_path_tmp.name)
114 |
115 | # Write error log
116 | print(f"Error: Cannot save file {file_path}.", flush=True)
117 | utils.write_error_log(ex)
118 |
119 | # Return error
120 | return json.dumps({"msg": "Error while saving file."})
121 |
122 | # Analyze file
123 | try:
124 | # Set config based on mdata
125 | if "lat" in mdata and "lon" in mdata:
126 | cfg.LATITUDE = float(mdata["lat"])
127 | cfg.LONGITUDE = float(mdata["lon"])
128 | else:
129 | cfg.LATITUDE = -1
130 | cfg.LONGITUDE = -1
131 |
132 | cfg.WEEK = int(mdata.get("week", -1))
133 | cfg.SIG_OVERLAP = max(0.0, min(2.9, float(mdata.get("overlap", 0.0))))
134 | cfg.SIGMOID_SENSITIVITY = max(0.5, min(1.0 - (float(mdata.get("sensitivity", 1.0)) - 1.0), 1.5))
135 | cfg.LOCATION_FILTER_THRESHOLD = max(0.01, min(0.99, float(mdata.get("sf_thresh", 0.03))))
136 |
137 | # Set species list
138 | if cfg.LATITUDE != -1 and cfg.LONGITUDE != -1:
139 | cfg.SPECIES_LIST_FILE = None
140 | cfg.SPECIES_LIST = species.get_species_list(
141 | cfg.LATITUDE, cfg.LONGITUDE, cfg.WEEK, cfg.LOCATION_FILTER_THRESHOLD
142 | )
143 | else:
144 | cfg.SPECIES_LIST_FILE = None
145 | cfg.SPECIES_LIST = []
146 |
147 | # Analyze file
148 | success = analyze.analyze_file((file_path, cfg.get_config()))
149 |
150 | # Parse results
151 | if success:
152 | # Open result file
153 | output_path = success["audacity"]
154 | lines = utils.read_lines(output_path)
155 | pmode = mdata.get("pmode", "avg").lower()
156 |
157 | # Pool results
158 | if pmode not in ["avg", "max"]:
159 | pmode = "avg"
160 |
161 | num_results = min(99, max(1, int(mdata.get("num_results", 5))))
162 |
163 | results = result_pooling(lines, num_results, pmode)
164 |
165 | # Prepare response
166 | data = {"msg": "success", "results": results, "meta": mdata}
167 |
168 | # Save response as metadata file
169 | if mdata.get("save", False):
170 | with open(file_path.rsplit(".", 1)[0] + ".json", "w") as f:
171 | json.dump(data, f, indent=2)
172 |
173 | # Return response
174 | del data["meta"]
175 |
176 | return json.dumps(data)
177 |
178 | return json.dumps({"msg": "Error during analysis."})
179 |
180 | except Exception as e:
181 | # Write error log
182 | print(f"Error: Cannot analyze file {file_path}.", flush=True)
183 | utils.write_error_log(e)
184 |
185 | data = {"msg": f"Error during analysis: {e}"}
186 |
187 | return json.dumps(data)
188 | finally:
189 | if file_path_tmp:
190 | os.unlink(file_path_tmp.name)
191 |
--------------------------------------------------------------------------------
/birdnet_analyzer/search/__init__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.search.core import search
2 |
3 | __all__ = ["search"]
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/search/__main__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.search.cli import main
2 |
3 | main()
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/search/cli.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer import utils
2 |
3 |
4 | @utils.runtime_error_handler
5 | def main():
6 | from birdnet_analyzer import cli, search
7 |
8 | parser = cli.search_parser()
9 | args = parser.parse_args()
10 |
11 | search(**vars(args))
12 |
--------------------------------------------------------------------------------
/birdnet_analyzer/search/core.py:
--------------------------------------------------------------------------------
1 | from typing import Literal
2 |
3 |
4 | def search(
5 | output: str,
6 | database: str,
7 | queryfile: str,
8 | *,
9 | n_results: int = 10,
10 | score_function: Literal["cosine", "euclidean", "dot"] = "cosine",
11 | crop_mode: Literal["center", "first", "segments"] = "center",
12 | overlap: float = 0.0,
13 | ):
14 | """
15 | Executes a search query on a given database and saves the results as audio files.
16 | Args:
17 | output (str): Path to the output directory where the results will be saved.
18 | database (str): Path to the database file to search in.
19 | queryfile (str): Path to the query file containing the search input.
20 | n_results (int, optional): Number of top results to return. Defaults to 10.
21 | score_function (Literal["cosine", "euclidean", "dot"], optional):
22 | Scoring function to use for similarity calculation. Defaults to "cosine".
23 | crop_mode (Literal["center", "first", "segments"], optional):
24 | Mode for cropping audio segments. Defaults to "center".
25 | overlap (float, optional): Overlap ratio for audio segments. Defaults to 0.0.
26 | Raises:
27 | ValueError: If the database does not contain the required settings metadata.
28 | Notes:
29 | - The function creates the output directory if it does not exist.
30 | - It retrieves metadata from the database to configure the search, including
31 | bandpass filter settings and audio speed.
32 | - The results are saved as audio files in the specified output directory, with
33 | filenames containing the score, source file name, and time offsets.
34 | Returns:
35 | None
36 | """
37 | import os
38 |
39 | import birdnet_analyzer.config as cfg
40 | from birdnet_analyzer import audio
41 | from birdnet_analyzer.search.utils import get_search_results
42 |
43 | # Create output folder
44 | if not os.path.exists(output):
45 | os.makedirs(output)
46 |
47 | # Load the database
48 | db = get_database(database)
49 |
50 | try:
51 | settings = db.get_metadata("birdnet_analyzer_settings")
52 | except KeyError as e:
53 | raise ValueError("No settings present in database.") from e
54 |
55 | fmin = settings["BANDPASS_FMIN"]
56 | fmax = settings["BANDPASS_FMAX"]
57 | audio_speed = settings["AUDIO_SPEED"]
58 |
59 | # Execute the search
60 | results = get_search_results(queryfile, db, n_results, audio_speed, fmin, fmax, score_function, crop_mode, overlap)
61 |
62 | # Save the results
63 | for r in results:
64 | embedding_source = db.get_embedding_source(r.embedding_id)
65 | file = embedding_source.source_id
66 | filebasename = os.path.basename(file)
67 | filebasename = os.path.splitext(filebasename)[0]
68 | offset = embedding_source.offsets[0] * audio_speed
69 | duration = cfg.SIG_LENGTH * audio_speed
70 | sig, rate = audio.open_audio_file(file, offset=offset, duration=duration, sample_rate=None)
71 | result_path = os.path.join(output, f"{r.sort_score:.5f}_{filebasename}_{offset}_{offset + duration}.wav")
72 | audio.save_signal(sig, result_path, rate)
73 |
74 |
75 | def get_database(database_path):
76 | from perch_hoplite.db import sqlite_usearch_impl
77 |
78 | return sqlite_usearch_impl.SQLiteUsearchDB.create(database_path).thread_split()
79 |
--------------------------------------------------------------------------------
/birdnet_analyzer/search/utils.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from perch_hoplite.db import brutalism
3 | from perch_hoplite.db.search_results import SearchResult
4 | from scipy.spatial.distance import euclidean
5 |
6 | import birdnet_analyzer.config as cfg
7 | from birdnet_analyzer import audio, model
8 |
9 |
10 | def cosine_sim(a, b):
11 | if a.ndim == 2:
12 | return np.array([cosine_sim(a[i], b) for i in range(a.shape[0])])
13 | return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
14 |
15 |
16 | def euclidean_scoring(a, b):
17 | if a.ndim == 2:
18 | return np.array([euclidean_scoring(a[i], b) for i in range(a.shape[0])])
19 | return euclidean(a, b)
20 |
21 |
22 | def euclidean_scoring_inverse(a, b):
23 | return -euclidean_scoring(a, b)
24 |
25 |
26 | def get_query_embedding(queryfile_path):
27 | """
28 | Extracts the embedding for a query file. Reads only the first 3 seconds
29 | Args:
30 | queryfile_path: The path to the query file.
31 | Returns:
32 | The query embedding.
33 | """
34 |
35 | # Load audio
36 | sig, rate = audio.open_audio_file(
37 | queryfile_path,
38 | duration=cfg.SIG_LENGTH * cfg.AUDIO_SPEED if cfg.SAMPLE_CROP_MODE == "first" else None,
39 | fmin=cfg.BANDPASS_FMIN,
40 | fmax=cfg.BANDPASS_FMAX,
41 | speed=cfg.AUDIO_SPEED,
42 | )
43 |
44 | # Crop query audio
45 | if cfg.SAMPLE_CROP_MODE == "center":
46 | sig_splits = [audio.crop_center(sig, rate, cfg.SIG_LENGTH)]
47 | elif cfg.SAMPLE_CROP_MODE == "first":
48 | sig_splits = [audio.split_signal(sig, rate, cfg.SIG_LENGTH, cfg.SIG_OVERLAP, cfg.SIG_MINLEN)[0]]
49 | else:
50 | sig_splits = audio.split_signal(sig, rate, cfg.SIG_LENGTH, cfg.SIG_OVERLAP, cfg.SIG_MINLEN)
51 |
52 | samples = sig_splits
53 | data = np.array(samples, dtype="float32")
54 |
55 | return model.embeddings(data)
56 |
57 |
58 | def get_search_results(
59 | queryfile_path, db, n_results, audio_speed, fmin, fmax, score_function: str, crop_mode, crop_overlap
60 | ):
61 | # Set bandpass frequency range
62 | cfg.BANDPASS_FMIN = max(0, min(cfg.SIG_FMAX, int(fmin)))
63 | cfg.BANDPASS_FMAX = max(cfg.SIG_FMIN, min(cfg.SIG_FMAX, int(fmax)))
64 | cfg.AUDIO_SPEED = max(0.01, audio_speed)
65 | cfg.SAMPLE_CROP_MODE = crop_mode
66 | cfg.SIG_OVERLAP = max(0.0, min(2.9, float(crop_overlap)))
67 |
68 | # Get query embedding
69 | query_embeddings = get_query_embedding(queryfile_path)
70 |
71 | # Set score function
72 | if score_function == "cosine":
73 | score_fn = cosine_sim
74 | elif score_function == "dot":
75 | score_fn = np.dot
76 | elif score_function == "euclidean":
77 | score_fn = euclidean_scoring_inverse # TODO: this is a bit hacky since the search function expects the score to be high for similar embeddings
78 | else:
79 | raise ValueError("Invalid score function. Choose 'cosine', 'euclidean' or 'dot'.")
80 |
81 | db_embeddings_count = db.count_embeddings()
82 | n_results = min(n_results, db_embeddings_count - 1)
83 | scores_by_embedding_id = {}
84 |
85 | for embedding in query_embeddings:
86 | results, scores = brutalism.threaded_brute_search(db, embedding, n_results, score_fn)
87 | sorted_results = results.search_results
88 |
89 | if score_function == "euclidean":
90 | for result in sorted_results:
91 | result.sort_score *= -1
92 |
93 | for result in sorted_results:
94 | if result.embedding_id not in scores_by_embedding_id:
95 | scores_by_embedding_id[result.embedding_id] = []
96 | scores_by_embedding_id[result.embedding_id].append(result.sort_score)
97 |
98 | results = []
99 |
100 | for embedding_id, scores in scores_by_embedding_id.items():
101 | results.append(SearchResult(embedding_id, np.sum(scores) / len(query_embeddings)))
102 |
103 | reverse = score_function != "euclidean"
104 |
105 | results.sort(key=lambda x: x.sort_score, reverse=reverse)
106 |
107 | return results[0:n_results]
108 |
--------------------------------------------------------------------------------
/birdnet_analyzer/segments/__init__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.segments.core import segments
2 |
3 | __all__ = ["segments"]
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/segments/__main__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.segments.cli import main
2 |
3 | main()
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/segments/cli.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.utils import runtime_error_handler
2 |
3 |
4 | @runtime_error_handler
5 | def main():
6 | from birdnet_analyzer import cli, segments
7 |
8 | # Parse arguments
9 | parser = cli.segments_parser()
10 |
11 | args = parser.parse_args()
12 |
13 | segments(**vars(args))
14 |
--------------------------------------------------------------------------------
/birdnet_analyzer/segments/core.py:
--------------------------------------------------------------------------------
1 | def segments(
2 | audio_input: str,
3 | output: str | None = None,
4 | results: str | None = None,
5 | *,
6 | min_conf: float = 0.25,
7 | max_segments: int = 100,
8 | audio_speed: float = 1.0,
9 | seg_length: float = 3.0,
10 | threads: int = 1,
11 | ):
12 | """
13 | Processes audio files to extract segments based on detection results.
14 | Args:
15 | audio_input (str): Path to the input folder containing audio files.
16 | output (str | None, optional): Path to the output folder where segments will be saved.
17 | If not provided, the input folder will be used as the output folder. Defaults to None.
18 | results (str | None, optional): Path to the folder containing detection result files.
19 | If not provided, the input folder will be used. Defaults to None.
20 | min_conf (float, optional): Minimum confidence threshold for detections to be considered.
21 | Defaults to 0.25.
22 | max_segments (int, optional): Maximum number of segments to extract per audio file.
23 | Defaults to 100.
24 | audio_speed (float, optional): Speed factor for audio processing. Defaults to 1.0.
25 | seg_length (float, optional): Length of each audio segment in seconds. Defaults to 3.0.
26 | threads (int, optional): Number of CPU threads to use for parallel processing.
27 | Defaults to 1.
28 | Returns:
29 | None
30 | Notes:
31 | - The function uses multiprocessing for parallel processing if `threads` is greater than 1.
32 | - On Windows, due to the lack of `fork()` support, configuration items are passed to each
33 | process explicitly.
34 | - It is recommended to use this function on Linux for better performance.
35 | """
36 | from multiprocessing import Pool
37 |
38 | import birdnet_analyzer.config as cfg
39 | from birdnet_analyzer.segments.utils import (
40 | extract_segments,
41 | parse_files,
42 | parse_folders,
43 | )
44 |
45 | cfg.INPUT_PATH = audio_input
46 |
47 | if not output:
48 | cfg.OUTPUT_PATH = cfg.INPUT_PATH
49 | else:
50 | cfg.OUTPUT_PATH = output
51 |
52 | results = results if results else cfg.INPUT_PATH
53 |
54 | # Parse audio and result folders
55 | cfg.FILE_LIST = parse_folders(audio_input, results)
56 |
57 | # Set number of threads
58 | cfg.CPU_THREADS = threads
59 |
60 | # Set confidence threshold
61 | cfg.MIN_CONFIDENCE = min_conf
62 |
63 | # Parse file list and make list of segments
64 | cfg.FILE_LIST = parse_files(cfg.FILE_LIST, max_segments)
65 |
66 | # Set audio speed
67 | cfg.AUDIO_SPEED = audio_speed
68 |
69 | # Add config items to each file list entry.
70 | # We have to do this for Windows which does not
71 | # support fork() and thus each process has to
72 | # have its own config. USE LINUX!
73 | flist = [(entry, seg_length, cfg.get_config()) for entry in cfg.FILE_LIST]
74 |
75 | # Extract segments
76 | if cfg.CPU_THREADS < 2:
77 | for entry in flist:
78 | extract_segments(entry)
79 | else:
80 | with Pool(cfg.CPU_THREADS) as p:
81 | p.map(extract_segments, flist)
82 |
--------------------------------------------------------------------------------
/birdnet_analyzer/species/__init__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.species.core import species
2 |
3 | __all__ = ["species"]
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/species/__main__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.species.cli import main
2 |
3 | main()
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/species/cli.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.utils import runtime_error_handler
2 |
3 |
4 | @runtime_error_handler
5 | def main():
6 | from birdnet_analyzer import cli, species
7 |
8 | # Parse arguments
9 | parser = cli.species_parser()
10 |
11 | args = parser.parse_args()
12 |
13 | species(**vars(args))
14 |
--------------------------------------------------------------------------------
/birdnet_analyzer/species/core.py:
--------------------------------------------------------------------------------
1 | from typing import Literal
2 |
3 |
4 | def species(
5 | output: str,
6 | *,
7 | lat: float = -1,
8 | lon: float = -1,
9 | week: int = -1,
10 | sf_thresh: float = 0.03,
11 | sortby: Literal["freq", "alpha"] = "freq",
12 | ):
13 | """
14 | Retrieves and processes species data based on the provided parameters.
15 | Args:
16 | output (str): The output directory or file path where the results will be stored.
17 | lat (float, optional): Latitude of the location for species filtering. Defaults to -1 (no filtering by location).
18 | lon (float, optional): Longitude of the location for species filtering. Defaults to -1 (no filtering by location).
19 | week (int, optional): Week of the year for species filtering. Defaults to -1 (no filtering by time).
20 | sf_thresh (float, optional): Species frequency threshold for filtering. Defaults to 0.03.
21 | sortby (Literal["freq", "alpha"], optional): Sorting method for the species list.
22 | "freq" sorts by frequency, and "alpha" sorts alphabetically. Defaults to "freq".
23 | Raises:
24 | FileNotFoundError: If the required model files are not found.
25 | ValueError: If invalid parameters are provided.
26 | Notes:
27 | This function ensures that the required model files exist before processing.
28 | It delegates the main processing to the `run` function from `birdnet_analyzer.species.utils`.
29 | """
30 | from birdnet_analyzer.species.utils import run
31 | from birdnet_analyzer.utils import ensure_model_exists
32 |
33 | ensure_model_exists()
34 |
35 | run(output, lat, lon, week, sf_thresh, sortby)
36 |
--------------------------------------------------------------------------------
/birdnet_analyzer/species/utils.py:
--------------------------------------------------------------------------------
1 | """Module for predicting a species list.
2 |
3 | Can be used to predict a species list using coordinates and weeks.
4 | """
5 |
6 | import os
7 |
8 | import birdnet_analyzer.config as cfg
9 | from birdnet_analyzer import model, utils
10 |
11 |
12 | def get_species_list(lat: float, lon: float, week: int, threshold=0.05, sort=False) -> list[str]:
13 | """Predict a species list.
14 |
15 | Uses the model to predict the species list for the given coordinates and filters by threshold.
16 |
17 | Args:
18 | lat: The latitude.
19 | lon: The longitude.
20 | week: The week of the year [1-48]. Use -1 for year-round.
21 | threshold: Only values above or equal to threshold will be shown.
22 | sort: If the species list should be sorted.
23 |
24 | Returns:
25 | A list of all eligible species.
26 | """
27 | # Extract species from model
28 | pred = model.explore(lat, lon, week)
29 |
30 | # Make species list
31 | slist = [p[1] for p in pred if p[0] >= threshold]
32 |
33 | return sorted(slist) if sort else slist
34 |
35 |
36 | def run(output_path, lat, lon, week, threshold, sortby):
37 | """
38 | Generates a species list for a given location and time, and saves it to the specified output path.
39 | Args:
40 | output_path (str): The path where the species list will be saved. If it's a directory, the list will be saved as "species_list.txt" inside it.
41 | lat (float): Latitude of the location.
42 | lon (float): Longitude of the location.
43 | week (int): Week of the year (1-52) for which the species list is generated.
44 | threshold (float): Threshold for location filtering.
45 | sortby (str): Sorting criteria for the species list. Can be "freq" for frequency or any other value for alphabetical sorting.
46 | Returns:
47 | None
48 | """
49 | # Load eBird codes, labels
50 | cfg.LABELS = utils.read_lines(cfg.LABELS_FILE)
51 |
52 | # Set output path
53 | cfg.OUTPUT_PATH = output_path
54 |
55 | if os.path.isdir(cfg.OUTPUT_PATH):
56 | cfg.OUTPUT_PATH = os.path.join(cfg.OUTPUT_PATH, "species_list.txt")
57 |
58 | # Set config
59 | cfg.LATITUDE, cfg.LONGITUDE, cfg.WEEK = lat, lon, week
60 | cfg.LOCATION_FILTER_THRESHOLD = threshold
61 |
62 | print(f"Getting species list for {cfg.LATITUDE}/{cfg.LONGITUDE}, Week {cfg.WEEK}...", end="", flush=True)
63 |
64 | # Get species list
65 | species_list = get_species_list(
66 | cfg.LATITUDE, cfg.LONGITUDE, cfg.WEEK, cfg.LOCATION_FILTER_THRESHOLD, sortby != "freq"
67 | )
68 |
69 | print(f"Done. {len(species_list)} species on list.", flush=True)
70 |
71 | # Save species list
72 | with open(cfg.OUTPUT_PATH, "w") as f:
73 | for s in species_list:
74 | f.write(s + "\n")
75 |
--------------------------------------------------------------------------------
/birdnet_analyzer/train/__init__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.train.core import train
2 |
3 | __all__ = ["train"]
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/train/__main__.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.train.cli import main
2 |
3 | main()
4 |
--------------------------------------------------------------------------------
/birdnet_analyzer/train/cli.py:
--------------------------------------------------------------------------------
1 | from birdnet_analyzer.utils import runtime_error_handler
2 |
3 |
4 | @runtime_error_handler
5 | def main():
6 | from birdnet_analyzer import cli, train
7 |
8 | # Parse arguments
9 | parser = cli.train_parser()
10 |
11 | args = parser.parse_args()
12 |
13 | train(**vars(args))
14 |
--------------------------------------------------------------------------------
/birdnet_analyzer/train/core.py:
--------------------------------------------------------------------------------
1 | from typing import Literal
2 |
3 |
4 | def train(
5 | audio_input: str,
6 | output: str = "checkpoints/custom/Custom_Classifier",
7 | test_data: str | None = None,
8 | *,
9 | crop_mode: Literal["center", "first", "segments"] = "center",
10 | overlap: float = 0.0,
11 | epochs: int = 50,
12 | batch_size: int = 32,
13 | val_split: float = 0.2,
14 | learning_rate: float = 0.0001,
15 | use_focal_loss: bool = False,
16 | focal_loss_gamma: float = 2.0,
17 | focal_loss_alpha: float = 0.25,
18 | hidden_units: int = 0,
19 | dropout: float = 0.0,
20 | label_smoothing: bool = False,
21 | mixup: bool = False,
22 | upsampling_ratio: float = 0.0,
23 | upsampling_mode: Literal["repeat", "mean", "smote"] = "repeat",
24 | model_format: Literal["tflite", "raven", "both"] = "tflite",
25 | model_save_mode: Literal["replace", "append"] = "replace",
26 | cache_mode: Literal["load", "save"] | None = None,
27 | cache_file: str = "train_cache.npz",
28 | threads: int = 1,
29 | fmin: float = 0.0,
30 | fmax: float = 15000.0,
31 | audio_speed: float = 1.0,
32 | autotune: bool = False,
33 | autotune_trials: int = 50,
34 | autotune_executions_per_trial: int = 1,
35 | ):
36 | """
37 | Trains a custom classifier model using the BirdNET-Analyzer framework.
38 | Args:
39 | audio_input (str): Path to the training data directory.
40 | test_data (str, optional): Path to the test data directory. Defaults to None. If not specified, a validation split will be used.
41 | output (str, optional): Path to save the trained model. Defaults to "checkpoints/custom/Custom_Classifier".
42 | crop_mode (Literal["center", "first", "segments", "smart"], optional): Mode for cropping audio samples. Defaults to "center".
43 | overlap (float, optional): Overlap ratio for audio segments. Defaults to 0.0.
44 | epochs (int, optional): Number of training epochs. Defaults to 50.
45 | batch_size (int, optional): Batch size for training. Defaults to 32.
46 | val_split (float, optional): Fraction of data to use for validation. Defaults to 0.2.
47 | learning_rate (float, optional): Learning rate for the optimizer. Defaults to 0.0001.
48 | use_focal_loss (bool, optional): Whether to use focal loss for training. Defaults to False.
49 | focal_loss_gamma (float, optional): Gamma parameter for focal loss. Defaults to 2.0.
50 | focal_loss_alpha (float, optional): Alpha parameter for focal loss. Defaults to 0.25.
51 | hidden_units (int, optional): Number of hidden units in the model. Defaults to 0.
52 | dropout (float, optional): Dropout rate for regularization. Defaults to 0.0.
53 | label_smoothing (bool, optional): Whether to use label smoothing. Defaults to False.
54 | mixup (bool, optional): Whether to use mixup data augmentation. Defaults to False.
55 | upsampling_ratio (float, optional): Ratio for upsampling underrepresented classes. Defaults to 0.0.
56 | upsampling_mode (Literal["repeat", "mean", "smote"], optional): Mode for upsampling. Defaults to "repeat".
57 | model_format (Literal["tflite", "raven", "both"], optional): Format to save the trained model. Defaults to "tflite".
58 | model_save_mode (Literal["replace", "append"], optional): Save mode for the model. Defaults to "replace".
59 | cache_mode (Literal["load", "save"] | None, optional): Cache mode for training data. Defaults to None.
60 | cache_file (str, optional): Path to the cache file. Defaults to "train_cache.npz".
61 | threads (int, optional): Number of CPU threads to use. Defaults to 1.
62 | fmin (float, optional): Minimum frequency for bandpass filtering. Defaults to 0.0.
63 | fmax (float, optional): Maximum frequency for bandpass filtering. Defaults to 15000.0.
64 | audio_speed (float, optional): Speed factor for audio playback. Defaults to 1.0.
65 | autotune (bool, optional): Whether to use hyperparameter autotuning. Defaults to False.
66 | autotune_trials (int, optional): Number of trials for autotuning. Defaults to 50.
67 | autotune_executions_per_trial (int, optional): Number of executions per autotuning trial. Defaults to 1.
68 | Returns:
69 | None
70 | """
71 | import birdnet_analyzer.config as cfg
72 | from birdnet_analyzer.train.utils import train_model
73 | from birdnet_analyzer.utils import ensure_model_exists
74 |
75 | ensure_model_exists()
76 |
77 | # Config
78 | cfg.TRAIN_DATA_PATH = audio_input
79 | cfg.TEST_DATA_PATH = test_data
80 | cfg.SAMPLE_CROP_MODE = crop_mode
81 | cfg.SIG_OVERLAP = overlap
82 | cfg.CUSTOM_CLASSIFIER = output
83 | cfg.TRAIN_EPOCHS = epochs
84 | cfg.TRAIN_BATCH_SIZE = batch_size
85 | cfg.TRAIN_VAL_SPLIT = val_split
86 | cfg.TRAIN_LEARNING_RATE = learning_rate
87 | cfg.TRAIN_WITH_FOCAL_LOSS = use_focal_loss if use_focal_loss is not None else cfg.TRAIN_WITH_FOCAL_LOSS
88 | cfg.FOCAL_LOSS_GAMMA = focal_loss_gamma
89 | cfg.FOCAL_LOSS_ALPHA = focal_loss_alpha
90 | cfg.TRAIN_HIDDEN_UNITS = hidden_units
91 | cfg.TRAIN_DROPOUT = dropout
92 | cfg.TRAIN_WITH_LABEL_SMOOTHING = label_smoothing if label_smoothing is not None else cfg.TRAIN_WITH_LABEL_SMOOTHING
93 | cfg.TRAIN_WITH_MIXUP = mixup if mixup is not None else cfg.TRAIN_WITH_MIXUP
94 | cfg.UPSAMPLING_RATIO = upsampling_ratio
95 | cfg.UPSAMPLING_MODE = upsampling_mode
96 | cfg.TRAINED_MODEL_OUTPUT_FORMAT = model_format
97 | cfg.TRAINED_MODEL_SAVE_MODE = model_save_mode
98 | cfg.TRAIN_CACHE_MODE = cache_mode
99 | cfg.TRAIN_CACHE_FILE = cache_file
100 | cfg.TFLITE_THREADS = 1
101 | cfg.CPU_THREADS = threads
102 |
103 | cfg.BANDPASS_FMIN = fmin
104 | cfg.BANDPASS_FMAX = fmax
105 |
106 | cfg.AUDIO_SPEED = audio_speed
107 |
108 | cfg.AUTOTUNE = autotune
109 | cfg.AUTOTUNE_TRIALS = autotune_trials
110 | cfg.AUTOTUNE_EXECUTIONS_PER_TRIAL = autotune_executions_per_trial
111 |
112 | # Train model
113 | train_model()
114 |
--------------------------------------------------------------------------------
/birdnet_analyzer/translate.py:
--------------------------------------------------------------------------------
1 | """Module for translating species labels.
2 |
3 | Can be used to translate species names into other languages.
4 |
5 | Uses the requests to the eBird-API.
6 | """
7 |
8 | import json
9 | import os
10 | import urllib.request
11 |
12 | import birdnet_analyzer.config as cfg
13 | from birdnet_analyzer import utils
14 |
15 | LOCALES = [
16 | "af",
17 | "ar",
18 | "cs",
19 | "da",
20 | "de",
21 | "en_uk",
22 | "es",
23 | "fi",
24 | "fr",
25 | "hu",
26 | "it",
27 | "ja",
28 | "ko",
29 | "nl",
30 | "no",
31 | "pl",
32 | "pt_BR",
33 | "pt_PT",
34 | "ro",
35 | "ru",
36 | "sk",
37 | "sl",
38 | "sv",
39 | "th",
40 | "tr",
41 | "uk",
42 | "zh",
43 | ]
44 | """ Locales for 26 common languages (according to GitHub Copilot) """
45 |
46 | API_TOKEN = "yourAPIToken"
47 | """ Sign up for your personal access token here: https://ebird.org/api/keygen """
48 |
49 |
50 | def get_locale_data(locale: str):
51 | """Download eBird locale species data.
52 |
53 | Requests the locale data through the eBird API.
54 |
55 | Args:
56 | locale: Two character string of a language.
57 |
58 | Returns:
59 | A data object containing the response from eBird.
60 | """
61 | url = "https://api.ebird.org/v2/ref/taxonomy/ebird?cat=species&fmt=json&locale=" + locale
62 | header = {"X-eBirdAPIToken": API_TOKEN}
63 |
64 | req = urllib.request.Request(url, headers=header)
65 | response = urllib.request.urlopen(req)
66 |
67 | return json.loads(response.read())
68 |
69 |
70 | def translate(locale: str):
71 | """Translates species names for a locale.
72 |
73 | Translates species names for the given language with the eBird API.
74 |
75 | Args:
76 | locale: Two character string of a language.
77 |
78 | Returns:
79 | The translated list of labels.
80 | """
81 | print(f"Translating species names for {locale}...", end="", flush=True)
82 |
83 | # Get locale data
84 | data = get_locale_data(locale)
85 |
86 | # Create list of translated labels
87 | labels: list[str] = []
88 |
89 | for label in cfg.LABELS:
90 | has_translation = False
91 | for entry in data:
92 | if label.split("_", 1)[0] == entry["sciName"]:
93 | labels.append("{}_{}".format(label.split("_", 1)[0], entry["comName"]))
94 | has_translation = True
95 | break
96 | if not has_translation:
97 | labels.append(label)
98 |
99 | print("Done.", flush=True)
100 |
101 | return labels
102 |
103 |
104 | def save_labels_file(labels: list[str], locale: str):
105 | """Saves localized labels to a file.
106 |
107 | Saves the given labels into a file with the format:
108 | "{config.LABELSFILE}_{locale}.txt"
109 |
110 | Args:
111 | labels: List of labels.
112 | locale: Two character string of a language.
113 | """
114 | # Create folder
115 | os.makedirs(cfg.TRANSLATED_LABELS_PATH, exist_ok=True)
116 |
117 | # Save labels file
118 | fpath = os.path.join(
119 | cfg.TRANSLATED_LABELS_PATH, "{}_{}.txt".format(os.path.basename(cfg.LABELS_FILE).rsplit(".", 1)[0], locale)
120 | )
121 | with open(fpath, "w", encoding="utf-8") as f:
122 | for label in labels:
123 | f.write(label + "\n")
124 |
125 |
126 | if __name__ == "__main__":
127 | # Load labels
128 | cfg.LABELS = utils.read_lines(cfg.LABELS_FILE)
129 |
130 | # Translate labels
131 | for locale in LOCALES:
132 | labels = translate(locale)
133 | save_labels_file(labels, locale)
134 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/_static/BirdNET-Go-logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/BirdNET-Go-logo.webp
--------------------------------------------------------------------------------
/docs/_static/BirdNET_Guide-Introduction-NotebookLM.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/BirdNET_Guide-Introduction-NotebookLM.mp3
--------------------------------------------------------------------------------
/docs/_static/BirdNET_Guide-Segment_review-NotebookLM.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/BirdNET_Guide-Segment_review-NotebookLM.mp3
--------------------------------------------------------------------------------
/docs/_static/BirdNET_Guide-Training-NotebookLM.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/BirdNET_Guide-Training-NotebookLM.mp3
--------------------------------------------------------------------------------
/docs/_static/Muuttolintujen-Kevät.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/Muuttolintujen-Kevät.png
--------------------------------------------------------------------------------
/docs/_static/birdnet-icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnet-icon.ico
--------------------------------------------------------------------------------
/docs/_static/birdnet-pi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnet-pi.png
--------------------------------------------------------------------------------
/docs/_static/birdnet-tiny-forge-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnet-tiny-forge-logo.png
--------------------------------------------------------------------------------
/docs/_static/birdnet_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnet_logo.png
--------------------------------------------------------------------------------
/docs/_static/birdnetlib.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnetlib.png
--------------------------------------------------------------------------------
/docs/_static/birdnetr-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdnetr-logo.png
--------------------------------------------------------------------------------
/docs/_static/birdweather.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/birdweather.png
--------------------------------------------------------------------------------
/docs/_static/chirpity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/chirpity.png
--------------------------------------------------------------------------------
/docs/_static/css/custom.css:
--------------------------------------------------------------------------------
1 | .wy-table-responsive table td,
2 | .wy-table-responsive table th {
3 | white-space: normal;
4 | }
--------------------------------------------------------------------------------
/docs/_static/dawnchorus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/dawnchorus.png
--------------------------------------------------------------------------------
/docs/_static/dummy_birds_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/dummy_birds_image.png
--------------------------------------------------------------------------------
/docs/_static/dummy_frogs_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/dummy_frogs_image.png
--------------------------------------------------------------------------------
/docs/_static/dummy_project_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/dummy_project_image.png
--------------------------------------------------------------------------------
/docs/_static/ecopi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/ecopi.png
--------------------------------------------------------------------------------
/docs/_static/ecosound-web_logo_large_white_on_black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/ecosound-web_logo_large_white_on_black.png
--------------------------------------------------------------------------------
/docs/_static/faunanet_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/faunanet_logo.png
--------------------------------------------------------------------------------
/docs/_static/gui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/gui.png
--------------------------------------------------------------------------------
/docs/_static/haikubox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/haikubox.png
--------------------------------------------------------------------------------
/docs/_static/logo_birdnet_big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/logo_birdnet_big.png
--------------------------------------------------------------------------------
/docs/_static/ribbit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/ribbit.png
--------------------------------------------------------------------------------
/docs/_static/whobird.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/docs/_static/whobird.png
--------------------------------------------------------------------------------
/docs/best-practices.rst:
--------------------------------------------------------------------------------
1 | Best practices
2 | ==============
3 |
4 | .. toctree::
5 | :maxdepth: 1
6 |
7 | best-practices/species-lists
8 | best-practices/segment-review
9 | best-practices/training
10 | best-practices/embeddings
--------------------------------------------------------------------------------
/docs/best-practices/embeddings.rst:
--------------------------------------------------------------------------------
1 | Embedding Extraction and Search
2 | ===============================
3 |
4 | 1. Introduction
5 | ----------------
6 |
7 | The embeddings extraction and search feature allows you to quickly search for similar audio files in large datasets.
8 | This can be used to explore your data and help you to collect training data for building a custom classifier.
9 |
10 |
11 | 2. Extracting Embeddings and creating a Database
12 | -------------------------------------------------
13 |
14 | The first step is to create a database of embeddings from your audio files.
15 | In the GUI go to the Embeddings-Tab and then to the Extract-Section. There you can select the directory containing your audio files.
16 | As with the analysis feature this will go to your directory recursively and include all audio files from the selected folder and any subdirectories.
17 | After that choose the directory where your embeddings database should be created and specify a name for your database.
18 |
19 | You can further specify the following parameters for the extraction:
20 |
21 | - | **Overlap**: Audio is still processed in 3-second snippets. This parameter specifies the overlap between these snippets.
22 | - | **Batch size**: This can be adjusted to increase the performance of the extraction process depending on your hardware.
23 | - | **Audio speed modifier**: This can be used to speed up or slow down the audio during the extraction process to enable working with ultra- and infrasonic recordings.
24 | - | **Bandpass filter frequencies**: This sets the bandpass filter which is applied after the speed modifier, to further filter out unwanted frequencies.
25 |
26 | .. note::
27 | The audio speed and bandpass parameters will be stored in the database and will also applied during the search process.
28 |
29 | .. note::
30 | Due to limitations of the underlying hoplite database, multithreading is not supported for the extraction process.
31 |
32 | The database will be created as a folder with the specified name containing two files:
33 | - 'hoplite.sqlite'
34 | - 'usearch.index'
35 |
36 | 2.1. File output
37 | ^^^^^^^^^^^^^^^^^^^
38 |
39 | If you want to process the embeddings as files, you can also specify a folder for the file output. If no folder is specified the file output will be omitted.
40 | The file output will create an individual file for each 3 second audio snippet that is processed, containing the extracted embedding.
41 |
42 | The files will be named according to the following pattern: "{original_file_name}_{start}_{end}.birdnet.embeddings.txt".
43 |
44 | 3. Searching your database
45 | -------------------------------------------------
46 |
47 | The database can be searched in the GUI in the Search-Section of the Embeddings-Tab.
48 | To start the search first select the database you want to search in. As soon as the database is loaded the extraction settings and the number of embeddings in the database will be displayed.
49 |
50 | After that select a file as a query example to find similar sounds in the database.
51 | You can select the :doc:`crop mode <../implementation-details/crop-modes>` to specify how the example file will be cropped if it is longer than 3 seconds. For the segments crop mode the simliarity measure will be averaged over all segments of the query example.
52 |
53 | Further specify the maximum number of results you want to retrieve and the score-function.
54 |
55 | The following score functions are available:
56 |
57 | - | **Cosine**: Uses the cosine of the angle between the two embedding vectors. More similar vectors will result in a higher value.
58 | - | **Dot**: Uses the dot product of the two embedding vectors. As with the cosine measure, more similar vectors will result in a higher value, but longer vectors will also increase the score.
59 | - | **Euclidean**: Uses the euclidean distance between the two embedding vectors. More similar vectors will result in a lower value.
60 |
61 | Click the start search button to show the results. The results will be displayed over multiple pages.
62 | For each result a spectrogram of the corresponding audio snippet will be shown and the audio can be played back for reference.
63 |
64 | While inspecting the results you can mark them for export. After finishing the selection click the export button and choose a folder to save the results in.
65 | You can now use the data for training custom classifiers or for further analysis.
66 |
67 |
68 | 3.1 About audio speed and bandpass filter in search
69 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
70 |
71 | The audio speed and bandpass filter settings that were used for the extraction process will also be applied to the query example and the result-snippets.
72 | This will also be reflected in the spectrograms and the audio playback.
73 |
74 | However, the exported audio will always be in the original speed and without any bandpass filter applied.
75 | This is to ensure that the exported audio retains its original sample rate and also to make using it in the training process more intuitive.
76 |
77 | As the processed audio snippets are always 3 seconds long after the audio speed modifier, the exported audio files may be shorter or longer.
78 | For example with an audio speed modifier of 2, e.g. double speed, the exported audio will only be 1.5 seconds long.
79 |
80 | 3.2. Search via CLI
81 | ^^^^^^^^^^^^^^^^^^^
82 |
83 | Searching the database can also be done via the command line interface, although due to the missing interface a manual inspection and selection of the results is not possible.
84 | Visit the command line interface documentation for more information on how to use the CLI.
85 |
86 |
--------------------------------------------------------------------------------
/docs/best-practices/segment-review.rst:
--------------------------------------------------------------------------------
1 | Segment Review
2 | =================================
3 |
4 | Get started by listening to this AI-generated summary of segments review:
5 |
6 | .. raw:: html
7 |
8 |
12 |
13 | |
14 | | `Source: Google NotebookLM`
15 |
16 | 1. Prepare Audio and Result Files
17 | ---------------------------------
18 |
19 | - | **Collect Audio Recordings and Corresponding BirdNET Result Files**: Organize them into separate folders.
20 | - | **Result File Formats**: BirdNET-Analyzer typically produces result files with extensions ".BirdNET.txt" or ".BirdNET.csv". It can process various result file formats, including "table", "kaleidoscope", "csv", and "audacity".
21 | - | **Understanding Confidence Values**: Note that BirdNET confidence values are not probabilities and are not directly transferable between different species or recording conditions.
22 |
23 | 2. Using the "Segments" Function in the GUI or Command Line
24 | -----------------------------------------------------------
25 |
26 | - | **Segments Function**: BirdNET provides the "segments" function to create a collection of species-specific predictions that exceed a user-defined confidence value. This function is available in the graphical user interface (GUI) under the "segments" tab or via the "segments.py" script in the command line.
27 | - | **GUI Usage**: In the GUI, you can select audio, result, and output directories. You can also set additional parameters such as the minimum confidence value, the maximum number of segments per species, the audio speed, and the segment length.
28 |
29 | 3. Setting Parameters
30 | ---------------------
31 |
32 | - | **Minimum Confidence (min_conf)**: Set a minimum confidence value for predictions to be considered. Note that this value may vary by species. It is recommended to determine the threshold by reviewing precision and recall.
33 | - | **Maximum Number of Segments (num_seq)**: Specify how many segments per species should be extracted.
34 | - | **Audio Speed (audio_speed)**: Adjust the playback speed. Extracted segments will be saved with the adjusted speed (e.g., to listen to ultrsonic calls).
35 | - | **Segment Length (seq_length)**: Define how long the extracted audio segments should be. If you set to more than 3 seconds, each segment will be padded with audio from the source recording. For example, for 5-second segment length, 1 second of audio before and after each extracted segment will be included. For 7 seconds, 2 seconds will be included, and so on. The first and last segment of each audio file might be shorter than the specified length.
36 |
37 | 4. Extracting Segments
38 | ----------------------
39 |
40 | - | **Start the Extraction Process**: After setting all parameters, start the extraction process. BirdNET will create subfolders for each identified species and save audio clips of the corresponding recordings.
41 | - | **Progress Display**: The progress of the process will be displayed.
42 |
43 | 5. Reviewing Results
44 | --------------------
45 |
46 | - | **Manual Review of Audio Segments**: The resulting audio segments can be manually reviewed to assess the accuracy of the predictions. It is important to note that BirdNET confidence values are not probabilities but a measure of the algorithm's prediction reliability.
47 | - | **Systematic Review**: It is recommended to start with the highest confidence scores and work down to the lower scores.
48 | - | **File Naming**: Files are named with confidence values, allowing for sorting by values.
49 |
50 | 6. Using the Review Tab in the GUI
51 | ----------------------------------
52 |
53 | - | **Review Tab Overview**: The review tab in the GUI allows you to systematically review and label the extracted segments. It provides tools for visualizing spectrograms, listening to audio segments, and categorizing them as positive or negative detections.
54 | - | **Collect Segments**: Use the review tab to collect segments from the specified directory. You can shuffle the segments for a randomized review process.
55 | - | **Create Log Plot**: The review tab can generate a logistic regression plot to visualize the relationship between confidence values and the likelihood of correct detections.
56 | - **Review Process**:
57 |
58 | - | **Select Directory**: Choose the directory containing the segments to be reviewed.
59 | - | **Species Dropdown**: Select the species to review from the dropdown menu.
60 | - | **File Count Matrix**: View the count of files to be reviewed, positive detections, and negative detections.
61 | - | **Spectrogram and Audio**: Visualize the spectrogram and listen to the audio segment.
62 | - | **Label Segments**: Use the buttons to label segments as positive or negative detections. You can also use the left and right arrow keys to assign labels.
63 | - | **Undo**: Undo the last action if needed.
64 | - | **Download Plots**: Download the spectrogram and regression plots for further analysis.
65 |
66 | 7. Alternative Approaches
67 | -------------------------
68 |
69 | - | **Raven Pro**: BirdNET result tables can be imported into Raven Pro and reviewed using the selection review function.
70 | - | **Converting Confidence Values to Probabilities**: Another approach is converting confidence values to probabilities using logistic regression in R. However, this still requires manual evaluation of predictions.
71 |
72 | 8. Important Notes
73 | ------------------
74 |
75 | - | **Non-Transferability of Confidence Values**: BirdNET confidence values are not easily transferable between species.
76 | - | **Audio Quality**: The accuracy of results heavily depends on the quality of audio recordings, such as sample rate and microphone quality.
77 | - | **Environmental Factors**: Results can be influenced by the recording environment, such as wind or rain.
78 | - | **Standardized Test Data**: Using standardized test data for evaluation is important to make results comparable.
79 |
80 | This guide summarizes the best practices for using the "segments" function of BirdNET-Analyzer and emphasizes the need for careful interpretation of the results.
--------------------------------------------------------------------------------
/docs/best-practices/species-lists.rst:
--------------------------------------------------------------------------------
1 | Creating Your Own Species List
2 | ==============================
3 |
4 | When editing your own `species_list.txt` file, make sure to copy species names from the labels file of each model.
5 |
6 | You can find label files in the checkpoints folder, e.g., `checkpoints/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels.txt`.
7 |
8 | Species names need to consist of `scientific name_common name` to be valid.
9 |
10 | You can generate a species list for a given location using :ref:`species.py `.
11 |
12 | Practical Information and Considerations
13 | ----------------------------------------
14 |
15 | **Understanding the GeoModel**
16 |
17 | The BirdNET Species Range Model V2.4 - V2 uses eBird checklist frequency data to estimate the range of bird species and the probability of their occurrence given latitude, longitude, and week of the year. eBird relies on citizen scientists to collect bird species observations around the world. Due to biases in these data, some regions such as North and South America, Europe, India, and Australia are well represented in the data, while large parts of Africa or Asia are underrepresented.
18 |
19 | In cases where eBird does not have enough observations (i.e., checklists), the data "only" contain binary filter data of likely species that could occur in a given location. Therefore, the training data for our biodiversity model is a mixture of actual observations and filter data curated by experts. We included all locations for which at least 10 checklists are available for each week of the year, and randomly added other locations with a 3% probability.
20 |
21 | **Limitations of the GeoModel**
22 |
23 | - **Data Coverage**: The model works well in regions with good eBird data coverage, such as North and South America, Europe, India, and Australia. In other regions, the lack of eBird observations means the resulting species lists may not reflect actual probabilities of occurrence.
24 | - **Binary Filter Data**: In areas with insufficient eBird data, the model relies on binary filter data, which may not be as accurate as actual observations.
25 | - **Seasonal Variations**: The model accounts for seasonal variations in bird presence, but the accuracy depends on the availability of data for each week of the year.
26 |
27 | **Creating Custom Species Lists**
28 |
29 | If you know which species to expect in your area, it is recommended to compile your own species list. This can help improve the accuracy of BirdNET-Analyzer for your specific use case.
30 |
31 | 1. **Collect Species Names**: Use the labels file from the model checkpoints to get the correct species names. Ensure the names are in the format `scientific name_common name`.
32 | 2. **Generate Species List**: Use the `species.py` script to generate a species list for a given location and time. This script uses the GeoModel to predict species occurrence based on latitude, longitude, and week of the year.
33 |
34 | **Example of Training Data**
35 |
36 | Here is an example of what the training data for a given location (Chemnitz) looks like:
37 |
38 | .. code:: python
39 |
40 | 'gretit1': [72, 90, 98, 93, 96, 88, 95, 94, 99, 99, 93, 92, 90, 96, 85, 97, 89, 78, 67, 68, 48, 39, 35, 40, 49, 49, 49, 51, 48, 55, 55, 73, 60, 64, 62, 63, 72, 72, 72, 67, 66, 80, 63, 74, 67, 76, 88, 70],
41 | 'carcro1': [62, 81, 83, 82, 85, 75, 90, 75, 83, 80, 76, 80, 84, 90, 72, 73, 83, 67, 70, 75, 54, 48, 42, 55, 51, 53, 55, 49, 55, 53, 55, 62, 57, 55, 66, 69, 63, 65, 69, 63, 59, 74, 61, 63, 76, 79, 69, 60],
42 | 'eurbla': [55, 80, 84, 92, 71, 70, 72, 84, 85, 86, 82, 95, 88, 92, 86, 91, 90, 75, 87, 81, 84, 72, 69, 62, 67, 70, 57, 66, 55, 56, 49, 32, 36, 37, 41, 49, 55, 62, 57, 58, 41, 37, 58, 67, 69, 64, 69, 49],
43 | 'blutit': [67, 83, 92, 93, 96, 83, 87, 93, 96, 90, 82, 80, 84, 88, 58, 79, 74, 52, 46, 36, 34, 29, 25, 26, 39, 43, 36, 43, 47, 42, 49, 48, 49, 51, 45, 52, 61, 64, 55, 55, 65, 72, 62, 71, 66, 67, 69, 64],
44 | 'grswoo': [61, 84, 80, 80, 90, 83, 85, 77, 76, 82, 72, 77, 77, 78, 64, 76, 81, 69, 73, 75, 66, 44, 46, 41, 47, 41, 38, 44, 42, 42, 52, 68, 37, 35, 38, 43, 44, 41, 43, 41, 49, 61, 41, 49, 48, 47, 67, 47],
45 | 'cowpig1': [9, 10, 3, 3, 16, 16, 30, 54, 65, 61, 69, 76, 83, 81, 80, 86, 80, 71, 68, 78, 68, 69, 79, 68, 76, 69, 69, 79, 70, 70, 68, 73, 64, 63, 58, 54, 53, 49, 53, 56, 44, 21, 33, 38, 45, 43, 5, 11],
46 | 'eurnut2': [43, 76, 88, 82, 79, 78, 91, 84, 92, 86, 76, 77, 75, 85, 69, 75, 60, 34, 47, 58, 34, 24, 33, 33, 31, 23, 28, 25, 23, 21, 23, 52, 26, 26, 31, 28, 25, 29, 32, 23, 47, 46, 24, 31, 30, 36, 61, 53],
47 | 'comcha': [26, 33, 30, 33, 34, 34, 39, 48, 70, 75, 80, 83, 80, 90, 76, 85, 80, 74, 77, 74, 59, 52, 51, 40, 34, 44, 33, 31, 22, 15, 17, 21, 17, 18, 26, 34, 44, 48, 53, 49, 31, 27, 33, 39, 44, 39, 30, 28]
48 |
49 | **Example of Model Predictions**
50 |
51 | If we query the trained model for the same location as above, we get these values for great tits:
52 |
53 | .. code:: python
54 |
55 | 'gretit': [99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 98, 98, 98, 98, 98, 97, 97, 97, 97, 97, 97, 98, 98, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99]
56 |
57 | **Conclusion**
58 |
59 | Overall, the model works well in regions with good data coverage. In other regions, the lack of eBird observations means the resulting species lists may not reflect actual probabilities of occurrence. Nevertheless, these lists can be used to filter for species that may or may not occur in these locations.
60 |
61 | By understanding the limitations and capabilities of the GeoModel, you can make informed decisions when creating and using custom species lists for BirdNET-Analyzer.
62 |
63 | See this post in the discussion forum for more details: `Species range model details `_
--------------------------------------------------------------------------------
/docs/birdnet-tiny.rst:
--------------------------------------------------------------------------------
1 | BirdNET-Tiny
2 | ============
3 |
4 | We also provide training and deployment tools for microcontrollers. **BirdNET-Tiny Forge** simplifies the training of BirdNET-Tiny models and their deployment on embedded devices.
5 |
6 | .. image:: _static/birdnet-tiny-forge-logo.png
7 | :alt: BirdNET-Tiny Forge
8 | :align: center
9 | :width: 150px
10 |
11 | |
12 |
13 | **BirdNET-Tiny Forge** is a collaboration between the BirdNET-team and fold ecosystemics. Start building your own tiny models for bioacoustics here: `https://github.com/birdnet-team/BirdNET-Tiny-Forge `_.
14 |
15 | .. note::
16 |
17 | BirdNET-Tiny Forge is under active development, so you might encounter changes that could affect your current workflow.
18 | We recommend checking for updates regularly.
--------------------------------------------------------------------------------
/docs/birdnetr.rst:
--------------------------------------------------------------------------------
1 | BirdNET in R
2 | ============
3 |
4 | We do also provide a BirdNET package for R, which allows you to analyze audio recordings directly in R.
5 |
6 | .. image:: _static/birdnetr-logo.png
7 | :alt: BirdNET-Tiny Forge
8 | :align: center
9 | :width: 150px
10 |
11 | |
12 |
13 | **birdnetR** is geared towards providing a robust workflow for ecological data analysis in bioacoustic projects.
14 | While it covers essential functionalities, it doesn’t include all the features found in BirdNET-Analyzer.
15 | Some features might only be available in the BirdNET Analyzer and not in this package.
16 |
17 | .. note::
18 | Please note that birdnetR is under active development, so you might encounter changes that could affect your current workflow.
19 | We recommend checking for updates regularly.
20 |
21 | See our website for more information:
22 |
23 | `https://birdnet-team.github.io/birdnetR/index.html `_
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 |
9 | import os
10 | import sys
11 |
12 | sys.path.insert(0, os.path.abspath("."))
13 | sys.path.insert(1, os.path.abspath(".."))
14 |
15 | project = "BirdNET-Analyzer"
16 | copyright = "%Y, BirdNET-Team"
17 | author = "Stefan Kahl"
18 | version = "1.5.1"
19 |
20 | # -- General configuration ---------------------------------------------------
21 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
22 |
23 | extensions = [
24 | "sphinx.ext.intersphinx",
25 | "sphinxarg.ext",
26 | ]
27 |
28 | intersphinx_mapping = {
29 | "python": ("https://docs.python.org/3", None),
30 | "matplotlib": ("https://matplotlib.org/stable/", None),
31 | "numpy": ("https://numpy.org/doc/stable/", None),
32 | }
33 |
34 | templates_path = ["_templates"]
35 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
36 |
37 | # -- Options for HTML output -------------------------------------------------
38 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
39 |
40 | # :github_url: is meta data used to force the "Edit on GitHub" link to point to the exact url.
41 | # https://sphinx-rtd-theme.readthedocs.io/en/stable/configuring.html#file-wide-metadata
42 | rst_prolog = ":github_url: https://github.com/birdnet-team/BirdNET-Analyzer\n"
43 | html_theme = "sphinx_rtd_theme"
44 | html_favicon = "_static/birdnet-icon.ico"
45 | html_logo = "_static/birdnet_logo.png"
46 | html_static_path = ["_static"]
47 | html_css_files = ["css/custom.css"]
48 | html_theme_options = {"style_external_links": True}
49 | html_show_sourcelink = False
50 | html_show_sphinx = False
51 | html_extra_path = ["projects.html", "projects_data.js"]
52 |
--------------------------------------------------------------------------------
/docs/contribute.rst:
--------------------------------------------------------------------------------
1 | How To Contibute
2 | ================
3 |
4 | Feel free to clone the `repository `_ and contribute to the project. We are always looking for new ideas and improvements. If you have any questions, please don't hesitate to ask.
5 |
6 | Let us know if you have any ideas for new features or improvements or submit a pull request.
7 |
8 | **Help us to improve the documentation!**
9 |
10 | Install `sphinx` and all required themes + plugins with `pip install sphinx sphinx_rtd_theme sphinx-argparse`.
11 |
12 | Run `sphinx-build docs docs/_build`.
13 |
14 | Navigate to `BirdNET-Analyzer/docs/_build` and open `index.html` with a browser of your choice.
15 |
16 | Make your changes to the `.rst` files in the `docs` directory and submit a pull request.
--------------------------------------------------------------------------------
/docs/faq.rst:
--------------------------------------------------------------------------------
1 | FAQ
2 | ===
3 |
4 | We will answer frequently asked questions here. If you have a question that is not answered here, please let us know at `ccb-birdnet@cornell.edu `_.
5 |
6 | What is BirdNET-Analyzer?
7 | -------------------------
8 |
9 | BirdNET-Analyzer is a tool for analyzing bird sounds using machine learning models. It can identify bird species from audio recordings and provides various functionalities for training custom classifiers, extracting segments, and reviewing results.
10 |
11 | How do I install BirdNET-Analyzer?
12 | ----------------------------------
13 |
14 | BirdNET-Analyzer can be installed using different methods, including:
15 |
16 | - | **Raven Pro**: Follow the instructions provided in the Raven Pro documentation.
17 | - | **Python Package**: Install via pip using `pip install birdnet`.
18 | - | **Command Line**: Download the repository and run the scripts from the command line.
19 | - | **GUI**: Download the GUI version from the `releases page `_ and follow the installation instructions.
20 |
21 | What licenses are used in BirdNET-Analyzer?
22 | -------------------------------------------
23 |
24 | BirdNET-Analyzer source code is released under the **MIT License**. The models used in the project are licensed under the **Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0)**. Please review and adhere to the specific license terms provided with each model.
25 | Custom models trained with BirdNET-Analyzer are also subject to the same licensing terms.
26 |
27 | .. note:: Please note that all educational and research purposes are considered non-commercial use and it is therefore freely permitted to use BirdNET models in any way.
28 |
29 | Please get in touch if you have any questions or need further assistance.
30 |
31 | How can I contribute training data to BirdNET?
32 | ----------------------------------------------
33 |
34 | The prefered way to contribute labeled audio recordings is through the `Xeno-Canto `_ platform. We regularly download new recordings from Xeno-Canto to improve the models.
35 |
36 | Fully annotated soundscape recordings should be shared on Zenodo or other data repositories - this way, they can be used for training and validation by the BirdNET team and other researchers.
37 |
38 | If you have large amounts of validated detections, please get in touch with us at `ccb-birdnet@cornell.edu `_.
39 |
40 | What are the non-event classes in BirdNET?
41 | ------------------------------------------
42 |
43 | There are currently 11 non-event classes in BirdNET:
44 |
45 | * Human non-vocal_Human non-vocal
46 | * Human vocal_Human vocal
47 | * Human whistle_Human whistle
48 | * Noise_Noise
49 | * Dog_Dog
50 | * Engine_Engine
51 | * Environmental_Environmental
52 | * Fireworks_Fireworks
53 | * Gun_Gun
54 | * Power tools_Power tools
55 | * Siren_Siren
56 |
57 | `Noise_Noise` and `Environmental_Environmental` are auxiliary classes used for training and will never be predicted by the model.
--------------------------------------------------------------------------------
/docs/implementation-details.rst:
--------------------------------------------------------------------------------
1 | Implementation details
2 | ==============
3 |
4 | .. toctree::
5 | :maxdepth: 1
6 |
7 | implementation-details/crop-modes
--------------------------------------------------------------------------------
/docs/implementation-details/crop-modes.rst:
--------------------------------------------------------------------------------
1 | Crop Modes
2 | ===============================
3 |
4 | This page describes the different crop modes available for the training and embeddings-search feature the BirdNET-Analyzer.
5 | In general a crop mode selection will be available in cases where audio files longer than 3 seconds are processed.
6 | With the the crop mode you can specify how the audio files should be cropped into 3 second snippets.
7 |
8 | 1. Center
9 | ----------------
10 |
11 | This crop mode will take the center 3 seconds of the audio file.
12 |
13 | 2. First
14 | ----------------
15 |
16 | This crop mode will take the first 3 seconds of the audio file.
17 |
18 | 3. Segments
19 | ----------------
20 |
21 | With this crop mode you can also specify an overlap. The crop mode will then split the audio file into 3 second segments with the specified overlap.
22 | In the training feature this will result in multiple training examples that are generated from the same audio file.
23 | In the search feature the similarity measure will be averaged over all segments of the query example.
24 |
25 |
26 | 4. Smart
27 | ----------------
28 |
29 | # TODO
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | BirdNET-Analyzer Documentation
2 | ==============================
3 |
4 | Welcome to the BirdNET-Analyzer documentation! This guide provides detailed information on installing, configuring, and using BirdNET-Analyzer.
5 |
6 | .. toctree::
7 | :maxdepth: 1
8 | :caption: Contents:
9 |
10 | installation
11 | usage
12 | models
13 | best-practices
14 | implementation-details
15 | faq
16 | showroom
17 | birdnetr
18 | birdnet-tiny
19 | contribute
20 |
21 | Introduction
22 | ------------
23 |
24 | BirdNET-Analyzer is an open source tool for analyzing bird calls using machine learning models. It can process large amounts of audio recordings and identify (bird) species based on their calls.
25 |
26 | Get started by listening to this AI-generated introduction of the BirdNET-Analyzer:
27 |
28 | .. raw:: html
29 |
30 |
34 |
35 | |
36 | | `Source: Google NotebookLM`
37 |
38 | Citing BirdNET-Analyzer
39 | -----------------------
40 |
41 | Feel free to use BirdNET for your acoustic analyses and research. If you do, please cite as:
42 |
43 | .. code-block:: bibtex
44 |
45 | @article{kahl2021birdnet,
46 | title={BirdNET: A deep learning solution for avian diversity monitoring},
47 | author={Kahl, Stefan and Wood, Connor M and Eibl, Maximilian and Klinck, Holger},
48 | journal={Ecological Informatics},
49 | volume={61},
50 | pages={101236},
51 | year={2021},
52 | publisher={Elsevier}
53 | }
54 |
55 | About
56 | -----
57 |
58 | Developed by the `K. Lisa Yang Center for Conservation Bioacoustics `_ at the `Cornell Lab of Ornithology `_ in collaboration with `Chemnitz University of Technology `_.
59 |
60 | Go to https://birdnet.cornell.edu to learn more about the project.
61 |
62 | Want to use BirdNET to analyze a large dataset? Don't hesitate to contact us: ccb-birdnet@cornell.edu
63 |
64 | We also have a discussion forum on `Reddit `_ if you have a general question or just want to chat.
65 |
66 | Have a question, remark, or feature request? Please start a new issue thread to let us know. Feel free to submit a pull request.
67 |
68 | More tools and resources
69 | ------------------------
70 |
71 | We also provide Python and R packages to interact with BirdNET models, as well as training and deployment tools for microcontrollers. Make sure to check out our other repositories at `https://github.com/birdnet-team `_.
72 |
73 |
74 | Projects map
75 | ------------
76 |
77 | We have created an interactive map of projects that use BirdNET. If you are working on a project that uses BirdNET, please let us know and we can add your project to the map.
78 |
79 | You can access the map here: `Open projects map `_
80 |
81 | Please refer to the `projects map documentation `_ for more information on how to contribute.
82 |
83 | License
84 | -------
85 |
86 | **Source Code**: The source code for this project is licensed under the `MIT License `_
87 |
88 | **Models**: The models used in this project are licensed under the `Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0) `_
89 |
90 | Please ensure you review and adhere to the specific license terms provided with each model.
91 |
92 | *Please note that all educational and research purposes are considered non-commercial use and it is therefore freely permitted to use BirdNET models in any way.*
93 |
94 | Funding
95 | -------
96 |
97 | This project is supported by Jake Holshuh (Cornell class of ´69) and The Arthur Vining Davis Foundations.
98 | Our work in the K. Lisa Yang Center for Conservation Bioacoustics is made possible by the generosity of K. Lisa Yang to advance innovative conservation technologies to inspire and inform the conservation of wildlife and habitats.
99 |
100 | The development of BirdNET is supported by the German Federal Ministry of Education and Research through the project “BirdNET+” (FKZ 01|S22072).
101 | The German Federal Ministry for the Environment, Nature Conservation and Nuclear Safety contributes through the “DeepBirdDetect” project (FKZ 67KI31040E).
102 | In addition, the Deutsche Bundesstiftung Umwelt supports BirdNET through the project “RangerSound” (project 39263/01).
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | .. _installation:
2 |
3 | Download & Setup
4 | ================
5 |
6 | GUI installer
7 | -------------
8 |
9 | You can download the latest BirdNET-Analyzer installer for Windows and MacOS from our `Releases page `_. This installer provides an easy setup process for running BirdNET-Analyzer on your system. Make sure to check to select the correct installer for your system.
10 |
11 | .. note::
12 | | Installation was only tested on M1 and M2 chips.
13 | | Feedback on older Intel CPUs or newer M3 chips is welcome!
14 |
15 | Raven Pro
16 | ---------
17 |
18 | If you want to analyze audio files without any additional coding or package install, you can now use `Raven Pro software `_ to run BirdNET models.
19 | After download, BirdNET is available through the new "Learning detector" feature in Raven Pro.
20 |
21 | For more information on how to use this feature, please visit the `Raven Pro Knowledge Base `_.
22 |
23 | `Download the newest model version here `_, extract the zip-file and move the extracted folder to the Raven models folder.
24 | On Windows, the models folder is ``C:\Users\\Raven Pro 1.6\Models``. Start Raven Pro and select *BirdNET_GLOBAL_6K_V2.4_Model_Raven* as learning detector.
25 |
26 | Python Package
27 | --------------
28 |
29 | The easiest way to setup BirdNET on your machine is to install `birdnetlib `_ or `birdnet `_ through pip with:
30 |
31 | .. code-block:: bash
32 |
33 | pip install birdnetlib
34 |
35 | or
36 |
37 | .. code-block:: bash
38 |
39 | pip install birdnet
40 |
41 | Please take a look at the `birdnetlib user guide `_ on how to analyze audio with `birdnetlib`.
42 |
43 | When using the `birdnet`-package, you can run BirdNET with:
44 |
45 | .. code-block:: python
46 |
47 | from pathlib import Path
48 | from birdnet.models import ModelV2M4
49 |
50 | # create model instance for v2.4
51 | model = ModelV2M4()
52 |
53 | # predict species within the whole audio file
54 | species_in_area = model.predict_species_at_location_and_time(42.5, -76.45, week=4)
55 | predictions = model.predict_species_within_audio_file(
56 | Path("soundscape.wav"),
57 | filter_species=set(species_in_area.keys())
58 | )
59 |
60 | # get most probable prediction at time interval 0s-3s
61 | prediction, confidence = list(predictions[(0.0, 3.0)].items())[0]
62 | print(f"predicted '{prediction}' with a confidence of {confidence:.6f}")
63 | # predicted 'Poecile atricapillus_Black-capped Chickadee' with a confidence of 0.814056
64 |
65 | For more examples and documentation, make sure to visit `pypi.org/project/birdnet/ `_.
66 |
67 | For any feature request or questions regarding `birdnet`, please add an issue or PR at `github.com/birdnet-team/birdnet `_.
68 |
69 | Command line installation
70 | -------------------------
71 |
72 | Requires Python 3.10. or 3.11.
73 |
74 | Clone the repository
75 |
76 | .. code-block:: bash
77 |
78 | git clone https://github.com/birdnet-team/BirdNET-Analyzer.git
79 | cd BirdNET-Analyzer
80 |
81 | Install the packages
82 |
83 | .. code-block:: bash
84 |
85 | pip install .
86 |
87 | .. note::
88 |
89 | If you also want to use the GUI, you need to install the additional packages with: ``pip install .[gui]``.
90 | Same goes for server and training tools: ``pip install .[server]`` and ``pip install .[train]``.
91 |
92 | Use ``pip install .[all]`` to install all packages.
93 |
94 | When building a GUI for systems using GTK with pywebview, you may need to install additional packages: qtpy and PyGObject.
95 | Use the following command: ``pip install qtpy PyGObject``.
96 |
97 | Verify the installation
98 |
99 | .. code-block:: bash
100 |
101 | python -m birdnet_analyzer.analyze
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/models.rst:
--------------------------------------------------------------------------------
1 | Models
2 | ======
3 |
4 |
5 | V2.4, June 2023
6 | ---------------
7 |
8 | * more than 6,000 species worldwide
9 | * covers frequencies from 0 Hz to 15 kHz with two-channel spectrogram (one for low and one for high frequencies)
10 | * 0.826 GFLOPs, 50.5 MB as FP32
11 | * enhanced and optimized metadata model
12 | * global selection of species (birds and non-birds) with 6,522 classes (incl. 11 non-event classes)
13 |
14 | Technical Details
15 | ^^^^^^^^^^^^^^^^^
16 |
17 | * 48 kHz sampling rate (we up- and downsample automatically and can deal with artifacts from lower sampling rates)
18 | * we compute 2 mel spectrograms as input for the convolutional neural network:
19 |
20 | * first one has fmin = 0 Hz and fmax = 3000; nfft = 2048; hop size = 278; 96 mel bins
21 | * second one has fmin = 500 Hz and fmax = 15 kHz; nfft = 1024; hop size = 280; 96 mel bins
22 |
23 | * both spectrograms have a final resolution of 96x511 pixels
24 | * raw audio will be normalized between -1 and 1 before spectrogram conversion
25 | * we use non-linear magnitude scaling as mentioned in `Schlüter 2018 `_
26 | * V2.4 uses an EfficienNetB0-like backbone with a final embedding size of 1024
27 | * See `this comment `_ for more details
28 |
29 | Species range model V2.4 - V2, Jan 2024
30 | ---------------------------------------
31 |
32 | * updated species range model based on eBird data
33 | * more accurate (spatial) species range prediction
34 | * slightly increased long-tail distribution in the temporal resolution
35 | * see `this discussion post `_ for more details
36 |
37 |
38 | Using older models
39 | ------------------
40 |
41 | Older models can also be used as custom classifiers in the GUI or using the `--classifier` argument in the `birdnet_analyzer.analyze` command line interface.
42 |
43 | Just download your desired model version and unzip.
44 |
45 | * GUI: Select the \*_Model_FP32.tflite file under **Species selection / Custom classifier**
46 | * CLI: ``python -m birdnet_analyzer ... --classifier 'path_to_Model_FP32.tflite'``
47 |
48 | Model Version History
49 | ---------------------
50 |
51 | .. note:: All models listed here are licensed under the `Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0) `_.
52 |
53 | V2.4
54 | ^^^^
55 |
56 | - more than 6,000 species worldwide
57 | - covers frequencies from 0 Hz to 15 kHz with two-channel spectrogram (one for low and one for high frequencies)
58 | - 0.826 GFLOPs, 50.5 MB as FP32
59 | - enhanced and optimized metadata model
60 | - global selection of species (birds and non-birds) with 6,522 classes (incl. 11 non-event classes)
61 | - Download here: `BirdNET-Analyzer-V2.4.zip `_
62 |
63 | V2.3
64 | ^^^^
65 |
66 | - slightly larger (36.4 MB vs. 21.3 MB as FP32) but smaller computational footprint (0.698 vs. 1.31 GFLOPs) than V2.2
67 | - larger embedding size (1024 vs 320) than V2.2 (hence the bigger model)
68 | - enhanced and optimized metadata model
69 | - global selection of species (birds and non-birds) with 3,337 classes (incl. 11 non-event classes)
70 | - Download here: `BirdNET-Analyzer-V2.3.zip `_
71 |
72 | V2.2
73 | ^^^^
74 |
75 | - smaller (21.3 MB vs. 29.5 MB as FP32) and faster (1.31 vs 2.03 GFLOPs) than V2.1
76 | - maintains same accuracy as V2.1 despite more classes
77 | - global selection of species (birds and non-birds) with 3,337 classes (incl. 11 non-event classes)
78 | - Download here: `BirdNET-Analyzer-V2.2.zip `_
79 |
80 | V2.1
81 | ^^^^
82 |
83 | - same model architecture as V2.0
84 | - extended 2022 training data
85 | - global selection of species (birds and non-birds) with 2,434 classes (incl. 11 non-event classes)
86 | - Download here: `BirdNET-Analyzer-V2.1.zip `_
87 |
88 | V2.0
89 | ^^^^
90 |
91 | - same model design as 1.4 but a bit wider
92 | - extended 2022 training data
93 | - global selection of species (birds and non-birds) with 1,328 classes (incl. 11 non-event classes)
94 | - Download here: `BirdNET-Analyzer-V2.0.zip `_
95 |
96 | V1.4
97 | ^^^^
98 |
99 | - smaller, deeper, faster
100 | - only 30% of the size of V1.3
101 | - still linear spectrogram and EfficientNet blocks
102 | - extended 2021 training data
103 | - 1,133 birds and non-birds for North America and Europe
104 | - Download here: `BirdNET-Analyzer-V1.4.zip `_
105 |
106 | V1.3
107 | ^^^^
108 |
109 | - Model uses linear frequency scale for spectrograms
110 | - uses V2 fusion blocks and V1 efficient blocks
111 | - extended 2021 training data
112 | - 1,133 birds and non-birds for North America and Europe
113 | - Download here: `BirdNET-Analyzer-V1.3.zip `_
114 |
115 | V1.2
116 | ^^^^
117 |
118 | - Model based on EfficientNet V2 blocks
119 | - uses V2 fusion blocks and V1 efficient blocks
120 | - extended 2021 training data
121 | - 1,133 birds and non-birds for North America and Europe
122 | - Download here: `BirdNET-Analyzer-V1.2.zip `_
123 |
124 | V1.1
125 | ^^^^
126 |
127 | - Model based on Wide-ResNet (aka "App model")
128 | - extended 2021 training data
129 | - 1,133 birds and non-birds for North America and Europe
130 | - Download here: `BirdNET-Analyzer-V1.1.zip `_
131 |
132 | App Model
133 | ^^^^^^^^^
134 |
135 | - Model based on Wide-ResNet
136 | - ~3,000 species worldwide
137 | - currently deployed as BirdNET app model
138 | - Download here: `BirdNET-Analyzer-App-Model.zip `_
139 |
--------------------------------------------------------------------------------
/docs/projects.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BirdNET Projects Map
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
BirdNET Projects Map
26 |
27 |
28 |
29 |
projects worldwide
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
178 |
179 |
180 |
--------------------------------------------------------------------------------
/docs/showroom.rst:
--------------------------------------------------------------------------------
1 | Showroom
2 | ========
3 |
4 | BirdNET powers a number of fantastic community projects dedicated to bird song identification, all of which use models from this repository.
5 | These are some highlights, make sure to check them out!
6 |
7 | .. list-table::
8 | :widths: 25 75
9 |
10 | * - .. image:: _static/haikubox.png
11 | :alt: HaikuBox
12 | :align: center
13 | :target: https://haikubox.com/
14 |
15 | - | **HaikuBox**
16 |
17 | Once connected to your WiFi, Haikubox will listen for birds 24/7.
18 | When BirdNET finds a match between its thousands of labeled sounds and the birdsong in your yard, it identifies the bird species and shares a three-second audio clip to the Haikubox website and smartphone app.
19 |
20 | | Learn more at: `HaikuBox.com `_
21 |
22 | * - .. image:: _static/birdnet-pi.png
23 | :alt: BirdNET-Pi
24 | :align: center
25 | :target: https://birdnetpi.com/
26 |
27 | - | **BirdNET-Pi**
28 |
29 | Built on the TFLite version of BirdNET, this project uses pre-built TFLite binaries for Raspberry Pi to run on-device sound analyses.
30 | It is able to recognize bird sounds from a USB sound card in realtime and share its data with the rest of the world.
31 |
32 | | Learn more at: `BirdNETPi.com `_
33 |
34 | .. note:: You can find the most up-to-date version of BirdNET-PI at `github.com/Nachtzuster/BirdNET-Pi `_
35 |
36 |
37 | * - .. image:: _static/birdweather.png
38 | :alt: BirdWeather
39 | :align: center
40 | :target: https://app.birdweather.com/
41 |
42 | - | **BirdWeather**
43 |
44 | This site was built to be a living library of bird vocalizations.
45 | Using the BirdNET artificial neural network, BirdWeather is continuously listening to over 1,000 active stations around the world in real-time.
46 |
47 | | Learn more at: `BirdWeather.com `_
48 |
49 | * - .. image:: _static/birdnetlib.png
50 | :alt: birdnetlib
51 | :align: center
52 | :target: https://joeweiss.github.io/birdnetlib/
53 |
54 | - | **birdnetlib**
55 |
56 | A python api for BirdNET-Analyzer and BirdNET-Lite. ``birdnetlib`` provides a common interface for BirdNET-Analyzer and BirdNET-Lite.
57 |
58 | | Learn more at: `github.io/birdnetlib `_
59 |
60 | * - .. image:: _static/ecopi.png
61 | :alt: ecoPi:Bird
62 | :align: center
63 | :target: https://oekofor.netlify.app/en/portfolio/ecopi-bird_en/
64 |
65 | - | **ecoPi:Bird**
66 |
67 | The ecoPi:Bird is a device for automated acoustic recordings of bird songs and calls, with a self-sufficient power supply.
68 | It facilitates economical long-term monitoring, implemented with minimal personal requirements.
69 |
70 | | Learn more at: `oekofor.netlify.app `_
71 |
72 | * - .. image:: _static/dawnchorus.png
73 | :alt: Dawn Chorus
74 | :align: center
75 | :target: https://dawn-chorus.org/en/
76 |
77 | - | **Dawn Chorus**
78 |
79 | Dawn Chorus invites global participation to record bird sounds for biodiversity research, art, and raising awareness.
80 | This project aims to sharpen our senses and creativity by connecting us more deeply with the wonders of nature.
81 |
82 | | Learn more at: `dawn-chorus.org `_
83 |
84 | * - .. image:: _static/chirpity.png
85 | :alt: Chirpity
86 | :align: center
87 | :target: https://chirpity.mattkirkland.co.uk/
88 |
89 | - | **Chirpity**
90 |
91 | Chirpity is a desktop application available for Windows, Mac and Linux platforms.
92 | Optimized for speed and ease of use, it can analyze anything from short clips to hundreds of hours of audio with unparalleled speed.
93 | Detections can be validated against reference calls, edited and saved to a call library.
94 | Results can also be exported to a variety of formats including CSV, Raven and eBird.
95 |
96 | | Learn more at: `chirpity.mattkirkland.co.uk `_
97 |
98 | * - .. image:: _static/BirdNET-Go-logo.webp
99 | :alt: BirdNET-Go
100 | :align: center
101 | :target: https://github.com/tphakala/go-birdnet
102 |
103 | - | **BirdNET-Go**
104 |
105 | Go-BirdNET is an application inspired by BirdNET-Analyzer.
106 | While the original BirdNET is based on Python, Go-BirdNET is built using Golang, aiming for simplified deployment across multiple platforms, from Windows PCs to single board computers like Raspberry Pi.
107 |
108 | | Learn more at: `github.com/tphakala/go-birdnet `_
109 |
110 | * - .. image:: _static/whobird.png
111 | :alt: whoBIRD
112 | :align: center
113 | :target: https://github.com/woheller69/whoBIRD
114 |
115 | - | **whoBIRD**
116 |
117 | whoBIRD empowers you to identify birds anywhere, anytime, without an internet connection.
118 | Built upon the TFLite version of BirdNET, this Android application harnesses the power of machine learning to recognize birds directly on your device.
119 |
120 | | Learn more at: `whoBIRD `_
121 |
122 | * - .. image:: _static/Muuttolintujen-Kevät.png
123 | :alt: Muuttolintujen Kevät
124 | :align: center
125 | :target: https://www.jyu.fi/en/research/muuttolintujen-kevat
126 |
127 | - | **Muuttolintujen Kevät**
128 |
129 | Muuttolintujen Kevät (Migration Birds Spring) is a mobile application developed at the University of Jyväskylä, enabling users to record bird songs and make bird observations using a re-trained version of BirdNET.
130 |
131 | | Learn more at: `jyu.fi `_
132 |
133 | * - .. image:: _static/faunanet_logo.png
134 | :alt: faunanet
135 | :align: center
136 | :target: https://github.com/ssciwr/faunanet
137 |
138 | - | **FaunaNet**
139 |
140 | faunanet provides a platform for bioacoustics research projects and is an extension of Birdnet-Analyzer based on birdnetlib.
141 | faunanet is written in pure Python and is developed by the Scientific Software Center at the University of Heidelberg, Germany.
142 |
143 | | Learn more at: `faunanet `_
144 |
145 | * - .. image:: _static/ecosound-web_logo_large_white_on_black.png
146 | :alt: ecoSound-web
147 | :align: center
148 | :target: https://ecosound-web.de/ecosound_web/
149 |
150 | - | **ecoSound-web**
151 |
152 | ecoSound-web is a web application for ecoacoustics to manage, re-sample, navigate, visualize, annotate, and analyze soundscape recordings.
153 | It can execute BirdNET on recording batches and is currently being developed at INRAE, France.
154 |
155 | | Learn more at: `F1000Research `_ and `GitHub `_
156 |
157 | * - .. image:: _static/ribbit.png
158 | :alt: Ribbit
159 | :align: center
160 | :target: https://ribbit.edi.eco/
161 |
162 | - | **Ribbit**
163 |
164 | Record frog calls and the web app will tell you the species. Clip it, Ribbit!
165 | The app uses a custom classifier built with BirdNET embeddings to identify frog species and gives nature enthusiasts the possibility to learn more about amphibians.
166 |
167 | | Learn more at: `ribbit.edi.eco `_
168 |
169 | **Other cool projects:**
170 |
171 | * BirdCAGE is an application for monitoring the bird songs in audio streams: `BirdCAGE at GitHub `_
172 | * BattyBirdNET-Analyzer is a tool to assist in the automated classification of bat calls: `BattyBirdNET-Analyzer at GitHub `_
173 |
174 | Working on a cool project that uses BirdNET? Let us know and we can feature your project here.
175 |
--------------------------------------------------------------------------------
/docs/usage.rst:
--------------------------------------------------------------------------------
1 | Usage
2 | =====
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | usage/cli
8 | usage/gui
9 | usage/docker
10 | usage/projects-map
11 |
--------------------------------------------------------------------------------
/docs/usage/docker.rst:
--------------------------------------------------------------------------------
1 | Docker
2 | ======
3 |
4 | We are currently re-working our Docker setup. Please check back later for updates.
--------------------------------------------------------------------------------
/docs/usage/gui.rst:
--------------------------------------------------------------------------------
1 | GUI
2 | ===
3 |
4 | We provide a stand-alone GUI which lets you launch the analysis through a web interface.
5 |
6 | .. image:: ../_static/gui.png
7 | :alt: BirdNET-Analyzer GUI
8 | :align: center
9 |
10 |
11 | You need to install two additional packages in order to use the GUI with ``pip install pywebview gradio``
12 |
13 | Launch the GUI with ``python -m birdnet_analyzer.gui``.
14 |
15 | Set all folders and parameters, after that, click 'Analyze'. GUI items represent the command line arguments.
16 | For more information about the command line arguments, please refer to the :ref:`Command line interface documentation `.
17 |
18 | `Alternatively download the installer to run the GUI on your system`.
19 |
20 | Segment review
21 | --------------
22 |
23 | Please read the paper from `Connor M. Wood and Stefan Kahl: Guidelines for appropriate use of BirdNET scores and other detector outputs `_.
24 |
25 | The **Review** tab in the GUI is an implementation of the workflow described in the paper.
26 | It allows you to review the segments that were detected by BirdNET and to verify the segments manually.
27 | This can help you to choose an appropriate cut-off threshold for your specific use case.
28 |
29 | General workflow:
30 |
31 | 1. Use the **Segments** tab in the GUI or the :ref:`segments.py ` script to extract short audio segments for species detections.
32 | 2. Open the **Review** tab in the GUI and select the parent directory containing the directories for all the species you want to review.
33 | 3. Review the segments and manually check "positive" if the segment does contain target species or "negative" if it does not.
34 |
35 | For each selected sample the logistic regression curve is fitted and the threshold is calculated.
36 |
37 | GUI Language
38 | ------------
39 |
40 | The default language of the GUI is English, but you can change it to German, French, Chinese or Portuguese in the Settings tab of the GUI.
41 | If you want to contribute a translation to another language you, use the files inside the lang folder as a template.
42 | You can then send us the translated files or create a pull request.
43 | To check your translation, place your file inside the ``lang`` folder and start the GUI, your language should now be available in the **Settings** tab.
44 | After selecting your language, you should restart the GUI to apply the changes.
45 |
46 | We thank our collaborators for contributing translations:
47 |
48 | Chinese: Sunny Tseng (`@Sunny Tseng `_)
49 |
50 | French: `@FranciumSoftware `_
51 |
52 | Portuguese: Larissa Sugai (`@LSMSugai `_)
53 |
54 | Russian: Александр Цветков (cau@yandex.ru, radio call sign: R1BAF)
55 |
--------------------------------------------------------------------------------
/docs/usage/projects-map.rst:
--------------------------------------------------------------------------------
1 | Projects Map
2 | ============
3 |
4 | We want to highlight the many use cases and great projects in bioacoustics and conservation that use BirdNET in their workflow. Therefore, we have created an interactive map showing the approximate locations of these projects and some additional information.
5 |
6 | You can access the map here: `Open projects map <../projects.html>`_
7 |
8 | We will update the map regularly and also work on the visual representation. **However, we need your help to add more projects to the map and keep the information accurate**.
9 |
10 | There are three ways to contribute: you can submit a pull request with an additional entry in the `projects data `_ file, you can submit `this Google form `_, or you can simply reply in this thread and provide the following information:
11 |
12 | - Project name*
13 | - Organization/project lead*
14 | - Target species*
15 | - Country
16 | - Region/Location*
17 | - Latitude*
18 | - Longitude*
19 | - Contact
20 | - Website
21 | - Paper
22 | - Species Image URL
23 | - Species Image Credit
24 |
25 | The fields marked with an asterisk are required to place a valid mark on the map.
26 |
27 | If you would like to update a project or find that some information is incorrect, please reply to this thread as well.
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "birdnet_analyzer"
7 | version = "2.0.1"
8 | license = { text = "MIT" }
9 | description = "BirdNET analyzer for scientific audio data processing and bird classification."
10 | authors = [{ name = "Stefan Kahl" }]
11 | maintainers = [{ name = "Josef Haupt" }, { name = "Max Mauermann" }]
12 | keywords = ["birdnet", "birdnet-analyzer"]
13 | readme = "README.md"
14 | requires-python = ">=3.11"
15 | classifiers = [
16 | "Programming Language :: Python :: 3.11",
17 | "Operating System :: OS Independent",
18 | "Topic :: Multimedia :: Sound/Audio :: Analysis",
19 | "Topic :: Scientific/Engineering",
20 | "Topic :: Scientific/Engineering :: Information Analysis",
21 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
22 | ]
23 | dependencies = [
24 | "librosa",
25 | "resampy",
26 | "tensorflow==2.15.1",
27 | "scikit-learn==1.6.1",
28 | "tqdm",
29 | "pandas",
30 | "matplotlib",
31 | ]
32 |
33 | [project.optional-dependencies]
34 | train = ["keras-tuner"]
35 | server = ["bottle", "requests"]
36 | gui = [
37 | "birdnet-analyzer[train,embeddings]",
38 | "gradio==5.32.1",
39 | "pywebview",
40 | "plotly[express]",
41 | "pywin32;platform_system=='Windows'",
42 | "qtpy;platform_system=='Linux'",
43 | "PyGObject;platform_system=='Linux'",
44 | ]
45 | embeddings = ["perch-hoplite"]
46 | all = ["birdnet-analyzer[server,gui]"]
47 | docs = ["sphinx", "sphinx-rtd-theme", "sphinx-argparse"]
48 | tests = ["pytest"]
49 | dev = ["birdnet_analyzer[tests]", "birdnet_analyzer[docs]", "ruff"]
50 |
51 | [project.scripts]
52 | birdnet-analyze = "birdnet_analyzer.analyze.cli:main"
53 | birdnet-embeddings = "birdnet_analyzer.embeddings.cli:main"
54 | birdnet-evaluate = "birdnet_analyzer.evaluation.__init__:main"
55 | birdnet-search = "birdnet_analyzer.search.cli:main"
56 | birdnet-train = "birdnet_analyzer.train.cli:main"
57 | birdnet-segments = "birdnet_analyzer.segments.cli:main"
58 | birdnet-species = "birdnet_analyzer.species.cli:main"
59 |
60 | [project.gui-scripts]
61 | birdnet-gui = "birdnet_analyzer.gui.__init__:main"
62 |
63 | [project.urls]
64 | Homepage = "https://birdnet.cornell.edu/birdnet"
65 | Documentation = "https://birdnet-team.github.io/BirdNET-Analyzer/"
66 | Repository = "https://github.com/birdnet-team/BirdNET-Analyzer"
67 | Issues = "https://github.com/birdnet-team/BirdNET-Analyzer/issues"
68 | Download = "https://github.com/birdnet-team/BirdNET-Analyzer/releases/latest"
69 |
70 | [tool.setuptools]
71 | packages = [
72 | "birdnet_analyzer",
73 | "birdnet_analyzer.analyze",
74 | "birdnet_analyzer.gui",
75 | "birdnet_analyzer.embeddings",
76 | "birdnet_analyzer.search",
77 | "birdnet_analyzer.species",
78 | "birdnet_analyzer.segments",
79 | "birdnet_analyzer.train",
80 | "birdnet_analyzer.evaluation",
81 | "birdnet_analyzer.evaluation.preprocessing",
82 | "birdnet_analyzer.evaluation.assessment",
83 | ]
84 |
85 | [tool.setuptools.package-data]
86 | birdnet_analyzer = [
87 | "eBird_taxonomy_codes_2024E.json",
88 | "lang/*",
89 | "labels/**/*",
90 | "gui/assets/**/*",
91 | ]
92 |
93 | [tool.pytest.ini_options]
94 | testpaths = ["tests"]
95 | pythonpath = ["birdnet_analyzer"]
96 |
97 | [tool.ruff]
98 | exclude = ["conf.py"]
99 | line-length = 165
100 |
101 | [tool.ruff.lint]
102 | select = [
103 | "F",
104 | "B",
105 | "A",
106 | "C4",
107 | "T10",
108 | "EXE",
109 | "PIE",
110 | "PYI",
111 | "PT",
112 | "Q",
113 | "RSE",
114 | "RET",
115 | "SIM",
116 | "TID",
117 | "TD",
118 | "TC",
119 | #"PTH",
120 | "FLY",
121 | "I",
122 | "NPY",
123 | "PD",
124 | #"N",
125 | "PERF",
126 | "E",
127 | "W",
128 | #"D",
129 | "PL",
130 | "UP",
131 | "FURB",
132 | "RUF",
133 | ]
134 | ignore = [
135 | "B008",
136 | "TD003",
137 | "TD002",
138 | "PD901",
139 | "SIM108",
140 | "E722",
141 | "PLR2004",
142 | "PLR0913",
143 | "PLR0915",
144 | "PLR0912",
145 | "PLC0206",
146 | "RUF015",
147 | ]
148 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/__init__.py
--------------------------------------------------------------------------------
/tests/analyze/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/analyze/__init__.py
--------------------------------------------------------------------------------
/tests/embeddings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/embeddings/__init__.py
--------------------------------------------------------------------------------
/tests/embeddings/test_embeddings.py:
--------------------------------------------------------------------------------
1 | import multiprocessing
2 | import os
3 | import shutil
4 | import tempfile
5 | from unittest.mock import MagicMock, patch
6 |
7 | import pytest
8 |
9 | import birdnet_analyzer.config as cfg
10 | from birdnet_analyzer.cli import embeddings_parser
11 | from birdnet_analyzer.embeddings.core import embeddings
12 |
13 |
14 | @pytest.fixture
15 | def setup_test_environment():
16 | # Create a temporary directory for testing
17 | test_dir = tempfile.mkdtemp()
18 | input_dir = os.path.join(test_dir, "input")
19 | output_dir = os.path.join(test_dir, "output")
20 |
21 | os.makedirs(input_dir, exist_ok=True)
22 | os.makedirs(output_dir, exist_ok=True)
23 |
24 | # Store original config values
25 | original_config = {attr: getattr(cfg, attr) for attr in dir(cfg) if not attr.startswith("_") and not callable(getattr(cfg, attr))}
26 |
27 | yield {
28 | "test_dir": test_dir,
29 | "input_dir": input_dir,
30 | "output_dir": output_dir,
31 | }
32 |
33 | # Clean up
34 | shutil.rmtree(test_dir)
35 |
36 | # Restore original config
37 | for attr, value in original_config.items():
38 | setattr(cfg, attr, value)
39 |
40 |
41 | @patch("birdnet_analyzer.utils.ensure_model_exists")
42 | @patch("birdnet_analyzer.embeddings.utils.run")
43 | def test_embeddings_cli(mock_run_embeddings: MagicMock, mock_ensure_model: MagicMock, setup_test_environment):
44 | env = setup_test_environment
45 |
46 | mock_ensure_model.return_value = True
47 |
48 | parser = embeddings_parser()
49 | args = parser.parse_args(["--input", env["input_dir"], "-db", env["output_dir"]])
50 |
51 | embeddings(**vars(args))
52 |
53 | mock_ensure_model.assert_called_once()
54 | threads = min(8, max(1, multiprocessing.cpu_count() // 2))
55 | mock_run_embeddings.assert_called_once_with(env["input_dir"], env["output_dir"], 0, 1.0, 0, 15000, threads, 1, None)
56 |
--------------------------------------------------------------------------------
/tests/evaluation/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/evaluation/__init__.py
--------------------------------------------------------------------------------
/tests/evaluation/assessment/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/evaluation/assessment/__init__.py
--------------------------------------------------------------------------------
/tests/evaluation/preprocessing/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/evaluation/preprocessing/__init__.py
--------------------------------------------------------------------------------
/tests/gui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/gui/__init__.py
--------------------------------------------------------------------------------
/tests/gui/test_language.py:
--------------------------------------------------------------------------------
1 | import json
2 | from collections import defaultdict
3 | from pathlib import Path
4 |
5 | from birdnet_analyzer.gui.settings import LANG_DIR
6 |
7 |
8 | def test_language_keys():
9 | language_files = list(Path(LANG_DIR).glob("*.json"))
10 | key_collection = defaultdict(list)
11 |
12 | for language_file in language_files:
13 | with open(language_file, encoding="utf-8") as f:
14 | language_data = f.read()
15 | assert language_data, f"Language file {language_file} is empty."
16 |
17 | language_keys: dict = json.loads(language_data)
18 |
19 | for k, v in language_keys.items():
20 | assert isinstance(k, str), f"Key {k} in {language_file} is not a string."
21 | assert isinstance(v, str), f"Value for key {k} in {language_file} is not a string."
22 | assert k, f"Key in {language_file} is empty."
23 | assert v, f"Value for key {k} in {language_file} is empty."
24 | key_collection[k].append(language_file.stem)
25 |
26 | missing_keys = []
27 | for key, files in key_collection.items():
28 | if len(files) != len(language_files):
29 | missing_in = [f.stem for f in language_files if f.stem not in files]
30 | missing_keys.append((key, missing_in))
31 | assert not missing_keys, (
32 | "Not all keys are present in all language files.\n" +
33 | "\n".join(f"Key '{key}' missing in: {', '.join(missing_in)}" for key, missing_in in missing_keys)
34 | )
35 |
--------------------------------------------------------------------------------
/tests/segments/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/segments/__init__.py
--------------------------------------------------------------------------------
/tests/segments/test_segments.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import tempfile
4 | from unittest.mock import MagicMock, patch
5 |
6 | import pytest
7 |
8 | import birdnet_analyzer.config as cfg
9 | from birdnet_analyzer.cli import segments_parser
10 | from birdnet_analyzer.segments.core import segments
11 |
12 |
13 | @pytest.fixture
14 | def setup_test_environment():
15 | # Create a temporary directory for testing
16 | test_dir = tempfile.mkdtemp()
17 | input_dir = os.path.join(test_dir, "input")
18 | output_dir = os.path.join(test_dir, "output")
19 | results_dir = os.path.join(test_dir, "results")
20 |
21 | os.makedirs(input_dir, exist_ok=True)
22 | os.makedirs(output_dir, exist_ok=True)
23 | os.makedirs(results_dir, exist_ok=True)
24 |
25 | file_list = [
26 | {"audio": os.path.join(input_dir, "audio1.wav"), "result": os.path.join(results_dir, "result1.csv")},
27 | {"audio": os.path.join(input_dir, "audio2.wav"), "result": os.path.join(results_dir, "result2.csv")}
28 | ]
29 |
30 | # Store original config values
31 | original_config = {
32 | attr: getattr(cfg, attr) for attr in dir(cfg) if not attr.startswith("_") and not callable(getattr(cfg, attr))
33 | }
34 |
35 | yield {
36 | "test_dir": test_dir,
37 | "input_dir": input_dir,
38 | "output_dir": output_dir,
39 | "results_dir": results_dir,
40 | "file_list": file_list,
41 | }
42 |
43 | # Clean up
44 | shutil.rmtree(test_dir)
45 |
46 | # Restore original config
47 | for attr, value in original_config.items():
48 | setattr(cfg, attr, value)
49 |
50 | @patch("birdnet_analyzer.segments.utils.extract_segments")
51 | @patch("birdnet_analyzer.segments.utils.parse_files")
52 | @patch("birdnet_analyzer.segments.utils.parse_folders")
53 | def test_segments_cli(mock_parse_folders: MagicMock, mock_parse_files: MagicMock, mock_extract_segments: MagicMock, setup_test_environment):
54 | env = setup_test_environment
55 |
56 | parser = segments_parser()
57 | args = parser.parse_args([env["input_dir"],"--results", env["results_dir"] ,"--output", env["output_dir"], "--threads", "1"])
58 |
59 | mock_parse_files.return_value = env["file_list"]
60 |
61 | segments(**vars(args))
62 |
63 | mock_parse_folders.assert_called_once()
64 | mock_parse_files.assert_called_once()
65 | mock_extract_segments.assert_called()
66 |
--------------------------------------------------------------------------------
/tests/species/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/species/__init__.py
--------------------------------------------------------------------------------
/tests/species/test_species.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import tempfile
4 | from unittest.mock import patch
5 |
6 | import pytest
7 |
8 | import birdnet_analyzer.config as cfg
9 | from birdnet_analyzer.cli import species_parser
10 | from birdnet_analyzer.species.core import species
11 |
12 |
13 | @pytest.fixture
14 | def setup_test_environment():
15 | # Create a temporary directory for testing
16 | test_dir = tempfile.mkdtemp()
17 | output_dir = os.path.join(test_dir, "output")
18 |
19 | os.makedirs(output_dir, exist_ok=True)
20 |
21 | # Store original config values
22 | original_config = {
23 | attr: getattr(cfg, attr) for attr in dir(cfg) if not attr.startswith("_") and not callable(getattr(cfg, attr))
24 | }
25 |
26 | yield {
27 | "test_dir": test_dir,
28 | "output_dir": output_dir,
29 | }
30 |
31 | # Clean up
32 | shutil.rmtree(test_dir)
33 |
34 | # Restore original config
35 | for attr, value in original_config.items():
36 | setattr(cfg, attr, value)
37 |
38 | @patch("birdnet_analyzer.utils.ensure_model_exists")
39 | @patch("birdnet_analyzer.species.utils.run")
40 | def test_embeddings_cli(mock_run_species, mock_ensure_model, setup_test_environment):
41 | env = setup_test_environment
42 |
43 | mock_ensure_model.return_value = True
44 |
45 | parser = species_parser()
46 | args = parser.parse_args([env["output_dir"]])
47 |
48 | species(**vars(args))
49 |
50 | mock_ensure_model.assert_called_once()
51 | mock_run_species.assert_called_once_with(env["output_dir"], -1, -1, -1, 0.03, "freq")
52 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import birdnet_analyzer.config as cfg
4 | from birdnet_analyzer import utils
5 |
6 |
7 | def test_read_lines_label_files():
8 | labels = Path(cfg.TRANSLATED_LABELS_PATH).glob("*.txt")
9 | expected_lines = 6522
10 |
11 | original_lines = utils.read_lines(cfg.LABELS_FILE)
12 |
13 | assert len(original_lines) == expected_lines, f"Expected {expected_lines} lines in {cfg.LABELS_FILE}, but got {len(original_lines)}"
14 |
15 | original_labels = []
16 |
17 | for line in original_lines:
18 | names = line.split("_")
19 | assert len(names) == 2, f"Expected two names in {line}, but got {len(names)} in {cfg.LABELS_FILE}"
20 | original_labels.append(names)
21 |
22 | for label in labels:
23 | lines = utils.read_lines(label)
24 |
25 | for i, line in enumerate(lines):
26 | names = line.split("_")
27 | assert len(names) == 2, f"Expected two names in {line}, but got {len(names)} in {label}"
28 | assert original_labels[i][0] == names[0], f"Expected {original_labels[i][0]} but got {names[0]} in {label}"
29 |
--------------------------------------------------------------------------------
/tests/train/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/birdnet-team/BirdNET-Analyzer/dafa961f616810870a9e8cd1b3581bf1c76bc51b/tests/train/__init__.py
--------------------------------------------------------------------------------
/tests/train/test_train.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import tempfile
4 | from unittest.mock import patch
5 |
6 | import pytest
7 |
8 | import birdnet_analyzer.config as cfg
9 | from birdnet_analyzer.cli import train_parser
10 | from birdnet_analyzer.train.core import train
11 |
12 |
13 | @pytest.fixture
14 | def setup_test_environment():
15 | # Create a temporary directory for testing
16 | test_dir = tempfile.mkdtemp()
17 | input_dir = os.path.join(test_dir, "input")
18 | output_dir = os.path.join(test_dir, "output")
19 |
20 | os.makedirs(input_dir, exist_ok=True)
21 | os.makedirs(output_dir, exist_ok=True)
22 |
23 | classifier_output = os.path.join(output_dir, "classifier_output")
24 |
25 | # Store original config values
26 | original_config = {
27 | attr: getattr(cfg, attr) for attr in dir(cfg) if not attr.startswith("_") and not callable(getattr(cfg, attr))
28 | }
29 |
30 | yield {
31 | "test_dir": test_dir,
32 | "input_dir": input_dir,
33 | "output_dir": output_dir,
34 | "classifier_output": classifier_output,
35 | }
36 |
37 | # Clean up
38 | shutil.rmtree(test_dir)
39 |
40 | # Restore original config
41 | for attr, value in original_config.items():
42 | setattr(cfg, attr, value)
43 |
44 | @patch("birdnet_analyzer.utils.ensure_model_exists")
45 | @patch("birdnet_analyzer.train.utils.train_model")
46 | def test_train_cli(mock_train_model, mock_ensure_model, setup_test_environment):
47 | env = setup_test_environment
48 |
49 | mock_ensure_model.return_value = True
50 |
51 | parser = train_parser()
52 | args = parser.parse_args([env["input_dir"], "--output", env["classifier_output"]])
53 |
54 | train(**vars(args))
55 |
56 | mock_ensure_model.assert_called_once()
57 | mock_train_model.assert_called_once_with()
58 |
--------------------------------------------------------------------------------