Skip to content

Commit 7b1aef3

Browse files
committed
Added PB Geomodel
1 parent edc87f3 commit 7b1aef3

8 files changed

Lines changed: 307 additions & 8 deletions

File tree

docs/birdnet.geo_models.v3_0.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ birdnet.geo\_models.v3\_0.tf module
2020
:show-inheritance:
2121
:undoc-members:
2222

23+
birdnet.geo\_models.v3\_0.pb module
24+
------------------------------------
25+
26+
.. automodule:: birdnet.geo_models.v3_0.pb
27+
:members:
28+
:show-inheritance:
29+
:undoc-members:
30+
2331
Module contents
2432
---------------
2533

src/birdnet/geo/models/v3_0/pb.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from __future__ import annotations
2+
3+
import shutil
4+
import tempfile
5+
import zipfile
6+
from pathlib import Path
7+
8+
from ordered_set import OrderedSet
9+
10+
from birdnet.core.backends import (
11+
PBBackend,
12+
VersionedGeoBackendProtocol,
13+
)
14+
from birdnet.geo.models.v3_0.model import GeoDownloaderBaseV3_0
15+
from birdnet.globals import (
16+
MODEL_PRECISION_FP32,
17+
MODEL_PRECISIONS,
18+
)
19+
from birdnet.utils.helper import (
20+
check_protobuf_model_files_exist,
21+
download_file_tqdm,
22+
get_species_from_file,
23+
)
24+
from birdnet.utils.local_data import get_lang_dir, get_model_path
25+
26+
27+
class GeoPBDownloaderV3_0(GeoDownloaderBaseV3_0):
28+
@classmethod
29+
def _get_paths(cls) -> tuple[Path, Path]:
30+
model_path = get_model_path("geo", "3.0", "pb", MODEL_PRECISION_FP32)
31+
lang_dir = get_lang_dir("geo", "3.0", "pb")
32+
return model_path, lang_dir
33+
34+
@classmethod
35+
def _check_geo_model_available(cls) -> bool:
36+
model_path, lang_dir = cls._get_paths()
37+
38+
model_is_downloaded = True
39+
model_is_downloaded &= model_path.is_dir()
40+
model_is_downloaded &= check_protobuf_model_files_exist(model_path)
41+
42+
model_is_downloaded &= lang_dir.is_dir()
43+
for lang in cls.AVAILABLE_LANGUAGES:
44+
model_is_downloaded &= (lang_dir / f"{lang}.txt").is_file()
45+
46+
return model_is_downloaded
47+
48+
@classmethod
49+
def _download_model(cls) -> None:
50+
dl_url = "TODO" # TODO: update with real zenodo URL once published
51+
dl_size = 0 # TODO: update with real download size once published
52+
53+
with tempfile.TemporaryDirectory(prefix="birdnet_download") as temp_dir:
54+
zip_download_path = Path(temp_dir) / "download.zip"
55+
download_file_tqdm(
56+
dl_url,
57+
zip_download_path,
58+
download_size=dl_size,
59+
description="Downloading geo model v3.0 (pb)",
60+
)
61+
62+
print("Extracting...")
63+
extract_dir = Path(temp_dir) / "extracted"
64+
65+
with zipfile.ZipFile(zip_download_path, "r") as zip_ref:
66+
zip_ref.extractall(extract_dir)
67+
68+
geo_model_dl_dir = extract_dir / "meta-model" # TODO: update folder name if different
69+
species_dl_dir = extract_dir / "labels"
70+
71+
geo_model_dir, geo_lang_dir = cls._get_paths()
72+
geo_model_dir.parent.mkdir(parents=True, exist_ok=True)
73+
shutil.rmtree(geo_model_dir, ignore_errors=True)
74+
shutil.move(geo_model_dl_dir, geo_model_dir)
75+
76+
geo_lang_dir.parent.mkdir(parents=True, exist_ok=True)
77+
shutil.rmtree(geo_lang_dir, ignore_errors=True)
78+
shutil.move(species_dl_dir, geo_lang_dir)
79+
print("Extracted.")
80+
81+
@classmethod
82+
def get_model_path_and_labels(
83+
cls,
84+
lang: str,
85+
) -> tuple[Path, OrderedSet[str]]:
86+
if not cls._check_geo_model_available():
87+
cls._download_model()
88+
assert cls._check_geo_model_available()
89+
90+
model_dir, langs_path = cls._get_paths()
91+
92+
lang_file = langs_path / f"{lang}.txt"
93+
if not lang_file.is_file():
94+
raise ValueError(f"Language does not exist: {lang}")
95+
96+
labels = get_species_from_file(lang_file, encoding="utf8")
97+
return model_dir, labels
98+
99+
100+
class GeoPBBackendFP32V3_0(PBBackend, VersionedGeoBackendProtocol):
101+
def __init__(
102+
self,
103+
model_path: Path,
104+
device_name: str,
105+
half_precision: bool,
106+
) -> None:
107+
super().__init__(model_path, device_name, half_precision)
108+
109+
@classmethod
110+
def input_key(cls) -> str:
111+
return "MNET_INPUT" # TODO: update if key name differs in v3.0
112+
113+
@classmethod
114+
def prediction_signature_name(cls) -> str:
115+
return "serving_default"
116+
117+
@classmethod
118+
def prediction_key(cls) -> str:
119+
return "MNET_CLASS_ACTIVATION" # TODO: update if key name differs in v3.0
120+
121+
@classmethod
122+
def supports_encoding(cls) -> bool:
123+
return False
124+
125+
@classmethod
126+
def encoding_signature_name(cls) -> str | None:
127+
return None
128+
129+
@classmethod
130+
def encoding_key(cls) -> str | None:
131+
return None
132+
133+
@classmethod
134+
def precision(cls) -> MODEL_PRECISIONS:
135+
return MODEL_PRECISION_FP32

src/birdnet/model_loader.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from birdnet.geo.models.v3_0.model import (
5252
GeoModelV3_0,
5353
)
54+
from birdnet.geo.models.v3_0.pb import GeoPBBackendFP32V3_0, GeoPBDownloaderV3_0
5455
from birdnet.geo.models.v3_0.tf import (
5556
GeoTFBackendFP16V3_0,
5657
GeoTFBackendFP32V3_0,
@@ -715,9 +716,20 @@ def _load_geo_model_V3_0(
715716
},
716717
)
717718
elif backend == MODEL_BACKEND_PB:
718-
raise ValueError(
719-
"The geo model v3.0 does not support the 'pb' backend. "
720-
"Use 'tf' instead."
719+
if precision != MODEL_PRECISION_FP32:
720+
raise ValueError(
721+
f"Unsupported model precision for geo model: {precision}. "
722+
f"Currently supported precision is: {MODEL_PRECISION_FP32}."
723+
)
724+
725+
model_kwargs = _validate_kwargs_allowed(model_kwargs, None)
726+
727+
model_path, species_list = GeoPBDownloaderV3_0.get_model_path_and_labels(lang)
728+
return GeoModelV3_0.load(
729+
model_path,
730+
species_list,
731+
backend_type=GeoPBBackendFP32V3_0,
732+
backend_kwargs={},
721733
)
722734
else:
723735
raise AssertionError()
@@ -755,9 +767,21 @@ def _load_custom_geo_model_V3_0(
755767
check_validity=check_validity,
756768
)
757769
elif backend == MODEL_BACKEND_PB:
758-
raise ValueError(
759-
"The geo model v3.0 does not support the 'pb' backend. "
760-
"Use 'tf' instead."
770+
if precision != MODEL_PRECISION_FP32:
771+
raise ValueError(
772+
f"Unsupported model precision for geo model: {precision}. "
773+
f"Currently supported precision is: {MODEL_PRECISION_FP32}."
774+
)
775+
776+
model = _validate_pb_model_folder(model)
777+
model_kwargs = _validate_kwargs_allowed(model_kwargs, None)
778+
779+
return GeoModelV3_0.load_custom(
780+
model,
781+
species_list,
782+
backend_type=GeoPBBackendFP32V3_0,
783+
backend_kwargs={},
784+
check_validity=check_validity,
761785
)
762786
else:
763787
raise AssertionError()

src/birdnet_tests/geo_models/v3_0/model_py/test_predict/test_geo_predict_model.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,49 @@ def test_tf_fp32_gpu() -> None:
139139
assert result.model_path == model.model_path.absolute()
140140
assert result.model_version == "3.0"
141141
assert result.model_precision == "fp32"
142+
143+
144+
# --- PB ---
145+
146+
147+
def test_pb_cpu() -> None:
148+
model = load("geo", "3.0", "pb", precision="fp32")
149+
result = model.predict(20, 50, week=1, min_confidence=0.03, half_precision=False)
150+
151+
assert result.latitude == 20
152+
assert result.longitude == 50
153+
assert result.week == 1
154+
assert result.model_path == model.model_path.absolute()
155+
assert result.model_version == "3.0"
156+
assert result.model_precision == "fp32"
157+
158+
159+
@pytest.mark.gpu
160+
def test_pb_gpu() -> None:
161+
ensure_gpu_or_skip()
162+
163+
model = load("geo", "3.0", "pb", precision="fp32")
164+
result = model.predict(
165+
20, 50, week=1, min_confidence=0.03, half_precision=False, device="GPU"
166+
)
167+
168+
assert result.latitude == 20
169+
assert result.longitude == 50
170+
assert result.week == 1
171+
assert result.model_path == model.model_path.absolute()
172+
assert result.model_version == "3.0"
173+
assert result.model_precision == "fp32"
174+
175+
176+
def test_pb_cpu_half() -> None:
177+
model = load("geo", "3.0", "pb", precision="fp32")
178+
result = model.predict(
179+
20, 50, week=1, min_confidence=0.03, half_precision=True, device="CPU"
180+
)
181+
182+
assert result.latitude == 20
183+
assert result.longitude == 50
184+
assert result.week == 1
185+
assert result.model_path == model.model_path.absolute()
186+
assert result.model_version == "3.0"
187+
assert result.model_precision == "fp32"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from birdnet.geo.models.v3_0.pb import GeoPBDownloaderV3_0
2+
3+
4+
def xtest_double_download() -> None:
5+
# takes too long to run normally
6+
GeoPBDownloaderV3_0._download_model()
7+
GeoPBDownloaderV3_0._download_model()
8+
9+
10+
if __name__ == "__main__":
11+
xtest_double_download()

src/birdnet_tests/model_loader_py/geo/test_load_custom_v3_0.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from birdnet.geo.models.v3_0.model import GeoModelV3_0
6+
from birdnet.geo.models.v3_0.pb import GeoPBDownloaderV3_0
67
from birdnet.geo.models.v3_0.tf import GeoTFDownloaderV3_0
78
from birdnet.model_loader import load_custom
89
from birdnet.utils.local_data import get_lang_dir, get_model_path
@@ -108,3 +109,53 @@ def test_tf_type_with_precisions_is_correct() -> None:
108109
)
109110
is GeoModelV3_0
110111
)
112+
113+
114+
def test_load_custom_geo_model_v3_0_pb_fp32() -> None:
115+
GeoPBDownloaderV3_0.get_model_path_and_labels("en_us")
116+
model = load_custom(
117+
"geo",
118+
"3.0",
119+
"pb",
120+
get_model_path("geo", "3.0", "pb", "fp32"),
121+
get_lang_dir("geo", "3.0", "pb") / "en_us.txt",
122+
precision="fp32",
123+
check_validity=check_validity(),
124+
)
125+
assert isinstance(model, GeoModelV3_0)
126+
127+
128+
def test_pb_type_is_correct() -> None:
129+
GeoPBDownloaderV3_0.get_model_path_and_labels("en_us")
130+
assert (
131+
type(
132+
load_custom(
133+
"geo",
134+
"3.0",
135+
"pb",
136+
get_model_path("geo", "3.0", "pb", "fp32"),
137+
get_lang_dir("geo", "3.0", "pb") / "en_us.txt",
138+
check_validity=False,
139+
)
140+
)
141+
is GeoModelV3_0
142+
)
143+
144+
145+
def test_load_pb_with_custom_library_raises_error() -> None:
146+
ensure_litert_or_skip()
147+
148+
with pytest.raises(
149+
ValueError,
150+
match=r"Unexpected keyword arguments: library.",
151+
):
152+
GeoPBDownloaderV3_0.get_model_path_and_labels("en_us")
153+
load_custom(
154+
"geo",
155+
"3.0",
156+
"pb", # type: ignore
157+
get_model_path("geo", "3.0", "pb", "fp32"),
158+
get_lang_dir("geo", "3.0", "pb") / "en_us.txt",
159+
library="litert",
160+
check_validity=check_validity(),
161+
) # type: ignore

src/birdnet_tests/model_loader_py/geo/test_load_v3_0.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,20 @@
1212
def test_pb_v3_0_with_library_raises_error() -> None:
1313
ensure_litert_or_skip()
1414

15-
with pytest.raises(ValueError):
16-
load("geo", "3.0", "pb", precision="fp32")
15+
with pytest.raises(
16+
ValueError,
17+
match=r"Unexpected keyword arguments: library.",
18+
):
19+
load("geo", "3.0", "pb", precision="fp32", library="litert") # type: ignore
20+
21+
22+
@pytest.mark.load_model
23+
def test_v3_0_pb() -> None:
24+
try:
25+
model = load("geo", "3.0", "pb", precision="fp32")
26+
except ReadTimeout as e:
27+
pytest.fail(f"Model download timed out: {e}. Try again later.")
28+
assert isinstance(model, GeoModelV3_0)
1729

1830

1931
@pytest.mark.load_model
@@ -91,3 +103,14 @@ def test_types_with_precisions_are_correct() -> None:
91103
type(load("geo", "3.0", "tf", precision=cast(Literal["int8"], f"int{8}")))
92104
is GeoModelV3_0
93105
)
106+
107+
108+
def test_pb_type_is_correct() -> None:
109+
assert type(load("geo", "3.0", "pb")) is GeoModelV3_0
110+
111+
112+
def test_pb_type_with_precision_is_correct() -> None:
113+
assert (
114+
type(load("geo", "3.0", "pb", precision=cast(Literal["fp32"], f"fp{32}")))
115+
is GeoModelV3_0
116+
)

0 commit comments

Comments
 (0)