Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions birdnet_analyzer/analyze/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def analyze(
output: str | None = None,
*,
model: str = "birdnet",
birdnet: ACOUSTIC_MODEL_VERSIONS = "2.4",
birdnet: ACOUSTIC_MODEL_VERSIONS = "3.0",
min_conf: float = 0.25,
classifier: str | None = None,
cc_species_list: str | None = None,
Expand Down Expand Up @@ -111,16 +111,19 @@ def analyze(
species_list_file = slist if isinstance(slist, (str, Path)) else ""
rtypes: list[RESULT_TYPES] = [rtype] if isinstance(rtype, str) else rtype

if lat is not None and lon is not None:
species_from_location = lat is not None and lon is not None

if species_from_location:
if slist is not None:
raise ValueError(
"Cannot use both location (lat/lon) and custom species list (slist) "
"together."
)

slist = run_geomodel(
lat, lon, week=week, language=locale, threshold=sf_thresh
).to_set()
# The geo model is global and uses its own taxonomy; run_inference reconciles
# its species with the selected acoustic model by scientific name, so the geo
# label language is irrelevant here and left at its default.
slist = run_geomodel(lat, lon, week=week, threshold=sf_thresh).to_set()

predictions = run_inference(
audio_input,
Expand All @@ -139,6 +142,7 @@ def analyze(
classifier=classifier,
cc_species_list=cc_species_list,
version=birdnet,
match_species_by_scientific_name=species_from_location,
callback=on_update,
n_workers=n_workers,
n_producers=n_producers,
Expand Down
34 changes: 25 additions & 9 deletions birdnet_analyzer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@
from birdnet.globals import (
ACOUSTIC_MODEL_VERSIONS,
MODEL_LANGUAGE_EN_US,
MODEL_LANGUAGES,
)

from birdnet_analyzer.config import AUTOTUNE_METRICS, TRAINED_MODEL_OUTPUT_FORMATS
from birdnet_analyzer.config import (
ALL_MODEL_LANGUAGES,
AUTOTUNE_METRICS,
DEFAULT_ACOUSTIC_MODEL_VERSION,
GEO_MODEL_LANGUAGES,
TRAINED_MODEL_OUTPUT_FORMATS,
)
from birdnet_analyzer.logs import setup_logging

SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
Expand Down Expand Up @@ -138,8 +143,8 @@ def birdnet_arg():
p = argparse.ArgumentParser(add_help=False)
p.add_argument(
"--birdnet",
default="2.4",
const="2.4",
default=DEFAULT_ACOUSTIC_MODEL_VERSION,
const=DEFAULT_ACOUSTIC_MODEL_VERSION,
nargs="?",
choices=get_args(ACOUSTIC_MODEL_VERSIONS),
action=set_model_action("birdnet"),
Expand Down Expand Up @@ -431,19 +436,25 @@ def min_conf_args():
return p


def locale_args():
def locale_args(languages=None):
"""
Creates an argument parser for locale settings.
This function creates an argument parser with a single argument `--locale`
(or `-l`) which specifies the locale for translated species common names.
The default value is 'en' (US English). The available locale values include
'af', 'en_UK', 'de', 'it', and others.
The default value is 'en_us' (US English).

Args:
languages: The locale codes to offer. Defaults to the union of every model
version's languages; the birdnet library validates the concrete
(model version, locale) pair when the model is loaded. Pass a narrower
set for commands bound to a single model (e.g. the geo model).

Returns:
argparse.ArgumentParser: An argument parser with the locale argument.
"""
p = argparse.ArgumentParser(add_help=False)

locale_choices = get_args(get_args(MODEL_LANGUAGES)[0])
locale_choices = list(languages) if languages is not None else ALL_MODEL_LANGUAGES
p.add_argument(
"-l",
"--locale",
Expand Down Expand Up @@ -878,7 +889,12 @@ def species_parser():
"""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
parents=[species_list_args(), locale_args(), verbosity_args()],
parents=[
species_list_args(),
# The species list comes from the geo model, so only its languages apply.
locale_args(languages=GEO_MODEL_LANGUAGES),
verbosity_args(),
],
)

parser.add_argument(
Expand Down
42 changes: 39 additions & 3 deletions birdnet_analyzer/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
import os
from typing import Literal
from typing import Literal, get_args

from birdnet.globals import (
ACOUSTIC_MODEL_VERSIONS,
GEO_MODEL_VERSIONS,
MODEL_LANGUAGE_EN_US,
MODEL_LANGUAGES,
VALID_MODEL_LANGUAGES_V2_4,
VALID_MODEL_LANGUAGES_V3_0,
)


def _newest_version(versions: tuple[str, ...]) -> str:
"""Return the highest ``"<major>.<minor>"`` version from the given tuple."""
return max(versions, key=lambda v: tuple(int(part) for part in v.split(".")))

from birdnet.globals import MODEL_LANGUAGE_EN_US, MODEL_LANGUAGES

SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
RANDOM_SEED: int = 42
MODEL_VERSION: str = "V2.4"

# The acoustic/geo model version used by default. Derived from the versions the
# installed birdnet library ships, so the analyzer follows the newest model without
# a code change. The geo model is not offered as a choice: its newest version always
# replaces the older ones (see birdnet_analyzer.model_utils.run_geomodel).
DEFAULT_ACOUSTIC_MODEL_VERSION: str = _newest_version(get_args(ACOUSTIC_MODEL_VERSIONS))
DEFAULT_GEO_MODEL_VERSION: str = _newest_version(get_args(GEO_MODEL_VERSIONS))

# The languages each acoustic model version can label its predictions in. v2.4 and
# v3.0 support different sets, so the CLI/GUI offer their union and the birdnet
# library validates the concrete (version, language) pair when a model is loaded.
ACOUSTIC_MODEL_LANGUAGES: dict[str, list[str]] = {
"2.4": list(VALID_MODEL_LANGUAGES_V2_4),
"3.0": list(VALID_MODEL_LANGUAGES_V3_0),
}
ALL_MODEL_LANGUAGES: list[str] = sorted(
set(VALID_MODEL_LANGUAGES_V2_4) | set(VALID_MODEL_LANGUAGES_V3_0)
)

# Languages the (always newest) geo model can label its species list in. The v3.0
# geo and acoustic models share the same language set.
GEO_MODEL_LANGUAGES: list[str] = list(VALID_MODEL_LANGUAGES_V3_0)
Comment thread
Josef-Haupt marked this conversation as resolved.

MODEL_VERSION: str = f"V{DEFAULT_ACOUSTIC_MODEL_VERSION}"
SCORE_FUNCTIONS = Literal["cosine", "euclidean", "dot"]
CROP_MODES = Literal["center", "first", "segments"]
CODES_FILE: str = os.path.join(SCRIPT_DIR, "eBird_taxonomy_codes_2024E.json")
Expand Down
5 changes: 4 additions & 1 deletion birdnet_analyzer/gui/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ def run_analysis(
custom_classifier_file if selected_model == gu._CUSTOM_CLASSIFIER else None
)
use_perch = selected_model == gu._USE_PERCH
# Custom classifiers are trained on 2.4 embeddings and perch ignores the version,
# so only an explicit BirdNET model choice changes the acoustic model version.
birdnet_version = gu.birdnet_version(selected_model)
slist = species_list_file if species_list_choice == gu._CUSTOM_SPECIES else None
lat = lat if species_list_choice == gu._PREDICT_SPECIES else None # ty:ignore[invalid-assignment]
lon = lon if species_list_choice == gu._PREDICT_SPECIES else None # ty:ignore[invalid-assignment]
Expand Down Expand Up @@ -149,7 +152,7 @@ def run_analysis(
merge_consecutive=merge_consecutive,
additional_columns=additional_columns,
model="perch" if use_perch else "birdnet",
birdnet="2.4",
birdnet=birdnet_version,
classifier=custom_classifier,
cc_species_list=None, # always default search path in GUI currently
on_update=on_update,
Expand Down
8 changes: 6 additions & 2 deletions birdnet_analyzer/gui/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,8 +364,12 @@ def load_analysis_params(path: str) -> dict[str, Any]:
elif kwargs.get("model") == "perch":
values["model_selection_radio"] = _species_choice("use-perch")
elif "model" in kwargs:
# Not localized, must match gui.utils._USE_BIRDNET_2_4.
values["model_selection_radio"] = "BirdNET 2.4"
# Not localized, must match gui.utils._USE_BIRDNET_2_4 / _USE_BIRDNET_3_0.
# Files written before 3.0 have no version and were always 2.4.
version = str(kwargs.get("birdnet", "2.4"))
values["model_selection_radio"] = (
"BirdNET 3.0" if version == "3.0" else "BirdNET 2.4"
)

return values

Expand Down
62 changes: 51 additions & 11 deletions birdnet_analyzer/gui/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@

import gradio as gr
import webview
from birdnet.globals import MODEL_LANGUAGE_EN_US, MODEL_LANGUAGES
from birdnet.globals import ACOUSTIC_MODEL_VERSIONS, MODEL_LANGUAGE_EN_US

import birdnet_analyzer.config as cfg
import birdnet_analyzer.gui.localization as loc
import birdnet_analyzer.gui.state as gs
from birdnet_analyzer import settings, utils
Expand All @@ -31,7 +32,14 @@
_CUSTOM_CLASSIFIER = loc.localize("species-list-radio-option-custom-classifier")
_ALL_SPECIES = loc.localize("species-list-radio-option-all")
_USE_PERCH = loc.localize("species-list-radio-option-use-perch")
# BirdNET acoustic model choices. Not localized: the version number is the label.
_USE_BIRDNET_2_4 = "BirdNET 2.4"
_USE_BIRDNET_3_0 = "BirdNET 3.0"
_BIRDNET_MODEL_VERSIONS: dict[str, str] = {
_USE_BIRDNET_2_4: "2.4",
_USE_BIRDNET_3_0: "3.0",
}

_WINDOW: webview.Window | None = None
_URL = ""
_HEART_LOGO = "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgd2lkdGg9IjE2IiBkYXRhLXZpZXctY29tcG9uZW50PSJ0cnVlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Im04IDE0LjI1LjM0NS42NjZhLjc1Ljc1IDAgMCAxLS42OSAwbC0uMDA4LS4wMDQtLjAxOC0uMDFhNy4xNTIgNy4xNTIgMCAwIDEtLjMxLS4xNyAyMi4wNTUgMjIuMDU1IDAgMCAxLTMuNDM0LTIuNDE0QzIuMDQ1IDEwLjczMSAwIDguMzUgMCA1LjUgMCAyLjgzNiAyLjA4NiAxIDQuMjUgMSA1Ljc5NyAxIDcuMTUzIDEuODAyIDggMy4wMiA4Ljg0NyAxLjgwMiAxMC4yMDMgMSAxMS43NSAxIDEzLjkxNCAxIDE2IDIuODM2IDE2IDUuNWMwIDIuODUtMi4wNDUgNS4yMzEtMy44ODUgNi44MThhMjIuMDY2IDIyLjA2NiAwIDAgMS0zLjc0NCAyLjU4NGwtLjAxOC4wMS0uMDA2LjAwM2gtLjAwMlpNNC4yNSAyLjVjLTEuMzM2IDAtMi43NSAxLjE2NC0yLjc1IDMgMCAyLjE1IDEuNTggNC4xNDQgMy4zNjUgNS42ODJBMjAuNTggMjAuNTggMCAwIDAgOCAxMy4zOTNhMjAuNTggMjAuNTggMCAwIDAgMy4xMzUtMi4yMTFDMTIuOTIgOS42NDQgMTQuNSA3LjY1IDE0LjUgNS41YzAtMS44MzYtMS40MTQtMy0yLjc1LTMtMS4zNzMgMC0yLjYwOS45ODYtMy4wMjkgMi40NTZhLjc0OS43NDkgMCAwIDEtMS40NDIgMEM2Ljg1OSAzLjQ4NiA1LjYyMyAyLjUgNC4yNSAyLjVaIj48L3BhdGg+DQo8L3N2Zz4=" # noqa: E501
Expand Down Expand Up @@ -72,6 +80,16 @@
}


def is_birdnet_model(model_choice: str) -> bool:
"""Whether the selected model is an official BirdNET acoustic model."""
return model_choice in _BIRDNET_MODEL_VERSIONS


def birdnet_version(model_choice: str) -> str:
"""The acoustic model version for a BirdNET model choice (falls back to 2.4)."""
return _BIRDNET_MODEL_VERSIONS.get(model_choice, "2.4")


def spectrogram_settings() -> dict:
"""Reads the spectrogram settings the user chose in the settings tab.

Expand Down Expand Up @@ -534,20 +552,42 @@ def on_tab_select(value: gr.SelectData):


def model_choices():
"""Returns the models that can be selected on the current platform."""
values = [_USE_BIRDNET_2_4, _CUSTOM_CLASSIFIER, _USE_PERCH]
"""Returns the models that can be selected on the current platform.

The known BirdNET acoustic versions (newest first) are filtered down to those the
installed birdnet library ships, so a dropped version stops being offered without a
code change. A brand-new major version still needs a label added here.
"""
available = get_args(ACOUSTIC_MODEL_VERSIONS)
birdnet_models = [
label
for label, version in (
(_USE_BIRDNET_3_0, "3.0"),
(_USE_BIRDNET_2_4, "2.4"),
)
if version in available
]

values = [*birdnet_models, _CUSTOM_CLASSIFIER, _USE_PERCH]

if platform.system() == "Darwin":
values.pop() # TODO: Remove when tf 2.21+ is available on macOS
values.remove(_USE_PERCH) # TODO: Remove when tf 2.21+ is available on macOS

return values


def default_model():
"""The model selected by default: the newest available BirdNET acoustic model."""
choices = model_choices()

return _USE_BIRDNET_3_0 if _USE_BIRDNET_3_0 in choices else choices[0]


def sample_species_model_settings(state: TabState, opened=True):
# The model decides which sample and species settings are available, so it has to
# be known before those are built, even though it is shown below them.
is_perch = (
state.get("model_selection_radio", _USE_BIRDNET_2_4, choices=model_choices())
state.get("model_selection_radio", default_model(), choices=model_choices())
== _USE_PERCH
)

Expand Down Expand Up @@ -748,12 +788,12 @@ def locale(state: TabState, visible=True):
Returns:
The dropdown element.
"""
options = get_args(MODEL_LANGUAGES)[0]

# The union of every acoustic model version's languages; the birdnet library
# validates the concrete (model version, locale) pair when the model is loaded.
return state.persist(
"locale_dropdown",
gr.Dropdown,
choices=get_args(options),
choices=cfg.ALL_MODEL_LANGUAGES,
value=cast("str", MODEL_LANGUAGE_EN_US),
visible=visible,
label=loc.localize("analyze-locale-dropdown-label"),
Expand Down Expand Up @@ -963,7 +1003,7 @@ def model_selection(state: TabState, opened=True):
"model_selection_radio",
gr.Radio,
choices=model_choices(),
value=_USE_BIRDNET_2_4,
value=default_model(),
label=loc.localize("model-selection-radio-label"),
info=loc.localize("model-selection-radio-info"),
)
Expand Down Expand Up @@ -1015,7 +1055,7 @@ def on_custom_classifier_selection_click():
gr.update(value=labels, visible=True),
)

locale_settings = locale(state, visible=selected_model == _USE_BIRDNET_2_4)
locale_settings = locale(state, visible=is_birdnet_model(selected_model))

species_list_df = gr.List(
value=[],
Expand All @@ -1042,7 +1082,7 @@ def on_model_selection_change(choice: str, cc_state):
return (
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=choice == _USE_BIRDNET_2_4),
gr.update(visible=is_birdnet_model(choice)),
)

model_selection_radio.change(
Expand Down
Loading
Loading