@@ -27,6 +27,7 @@ def analyze(
2727 threads : int = 8 ,
2828 locale : str = "en" ,
2929 additional_columns : list [str ] | None = None ,
30+ use_perch : bool = False ,
3031):
3132 """
3233 Analyzes audio files for bird species detection using the BirdNET-Analyzer.
@@ -55,6 +56,7 @@ def analyze(
5556 threads (int, optional): Number of CPU threads to use for analysis. Defaults to 8.
5657 locale (str, optional): Locale for species names and output. Defaults to "en".
5758 additional_columns (list[str] | None, optional): Additional columns to include in the output. Defaults to None.
59+ use_perch (bool, optional): Whether to use the Perch model for analysis. Defaults to False.
5860 Returns:
5961 None
6062 Raises:
@@ -69,9 +71,6 @@ def analyze(
6971 import birdnet_analyzer .config as cfg
7072 from birdnet_analyzer .analyze .utils import analyze_file , save_analysis_params
7173 from birdnet_analyzer .analyze .utils import combine_results as combine
72- from birdnet_analyzer .utils import ensure_model_exists
73-
74- ensure_model_exists ()
7574
7675 flist = _set_params (
7776 audio_input = audio_input ,
@@ -98,6 +97,7 @@ def analyze(
9897 threads = threads ,
9998 labels_file = cfg .LABELS_FILE ,
10099 additional_columns = additional_columns ,
100+ use_perch = use_perch ,
101101 )
102102
103103 print (f"Found { len (cfg .FILE_LIST )} files to analyze" )
@@ -154,29 +154,37 @@ def _set_params(
154154 threads ,
155155 labels_file = None ,
156156 additional_columns = None ,
157+ use_perch = False ,
157158):
158159 import birdnet_analyzer .config as cfg
159160 from birdnet_analyzer .analyze .utils import load_codes
160161 from birdnet_analyzer .species .utils import get_species_list
161- from birdnet_analyzer .utils import collect_audio_files , read_lines
162+ from birdnet_analyzer .utils import collect_audio_files , ensure_model_exists , read_lines
163+
164+ ensure_model_exists (check_perch = use_perch )
162165
163166 if not isinstance (overlap , int | float ):
164167 raise ValueError ("Overlap must be a numeric value." )
165168
166169 if overlap < 0 :
167170 raise ValueError ("Overlap must be a non-negative value." )
168171
169- if overlap >= cfg .SIG_LENGTH :
170- raise ValueError (f"Overlap must be less than { cfg .SIG_LENGTH } seconds." )
172+ if not use_perch and overlap > 2.9 :
173+ raise ValueError ("Overlap must be less than or equal to 2.9 seconds for BirdNET model." )
174+
175+ if use_perch and overlap > 4.9 :
176+ raise ValueError ("Overlap must be less than or equal to 4.9 seconds for Perch model." )
171177
172178 if not isinstance (audio_speed , int | float ):
173179 raise ValueError ("Audio speed must be a numeric value." )
174180
175181 if audio_speed <= 0 :
176182 raise ValueError ("Audio speed must be a positive value." )
177183
184+ if use_perch and sensitivity != 1.0 :
185+ print ("Warning: Sensitivity setting is ignored when using the Perch model." )
186+
178187 cfg .CODES = load_codes ()
179- cfg .LABELS = read_lines (labels_file if labels_file else cfg .LABELS_FILE )
180188 cfg .SKIP_EXISTING_RESULTS = skip_existing_results
181189 cfg .LOCATION_FILTER_THRESHOLD = sf_thresh
182190 cfg .TOP_N = top_n
@@ -192,6 +200,10 @@ def _set_params(
192200 cfg .COMBINE_RESULTS = combine_results
193201 cfg .BATCH_SIZE = bs
194202 cfg .ADDITIONAL_COLUMNS = additional_columns
203+ cfg .USE_PERCH = use_perch
204+
205+ if cfg .USE_PERCH and custom_classifier :
206+ raise ValueError ("Selected custom classifier and Perch model, please select only one." )
195207
196208 if not output :
197209 if os .path .isfile (cfg .INPUT_PATH ):
@@ -213,47 +225,66 @@ def _set_params(
213225 cfg .CPU_THREADS = 1
214226 cfg .TFLITE_THREADS = threads
215227
216- if custom_classifier is not None :
228+ if cfg .USE_PERCH :
229+ cfg .MODEL_PATH = cfg .PERCH_V2_MODEL_PATH
230+ cfg .LABELS_FILE = cfg .PERCH_LABELS_FILE
231+ cfg .SAMPLE_RATE = cfg .PERCH_SAMPLE_RATE
232+ cfg .SIG_LENGTH = cfg .PERCH_SIG_LENGTH
233+ cfg .LABELS = read_lines (cfg .PERCH_LABELS_FILE )
234+ cfg .LABELS = cfg .LABELS [1 :] # it's a csv with header
235+ else :
236+ cfg .MODEL_PATH = cfg .BIRDNET_MODEL_PATH
237+ cfg .LABELS_FILE = cfg .BIRDNET_LABELS_FILE
238+ cfg .SAMPLE_RATE = cfg .BIRDNET_SAMPLE_RATE
239+ cfg .SIG_LENGTH = cfg .BIRDNET_SIG_LENGTH
240+ cfg .LABELS = read_lines (labels_file if labels_file else cfg .LABELS_FILE )
241+
242+ if overlap >= cfg .SIG_LENGTH :
243+ raise ValueError (f"Overlap must be less than { cfg .SIG_LENGTH } seconds." )
244+
245+ # Custom classifier trained with the Analyzer, not arbitrary models, meaning; A a tflite model or B a raven model
246+ if custom_classifier is None :
247+ # TODO: does species list even make sense with Perch?
248+ cfg .LATITUDE , cfg .LONGITUDE , cfg .WEEK = lat , lon , week
249+ cfg .CUSTOM_CLASSIFIER = None
250+
251+ if cfg .LATITUDE == - 1 and cfg .LONGITUDE == - 1 :
252+ if not slist :
253+ cfg .SPECIES_LIST_FILE = None
254+ else :
255+ cfg .SPECIES_LIST_FILE = slist
256+
257+ if os .path .isdir (cfg .SPECIES_LIST_FILE ):
258+ cfg .SPECIES_LIST_FILE = os .path .join (cfg .SPECIES_LIST_FILE , "species_list.txt" )
259+
260+ cfg .SPECIES_LIST = read_lines (cfg .SPECIES_LIST_FILE , trim = True , fail_on_blank_lines = True )
261+ else :
262+ cfg .SPECIES_LIST_FILE = None
263+ cfg .SPECIES_LIST = get_species_list (cfg .LATITUDE , cfg .LONGITUDE , cfg .WEEK , cfg .LOCATION_FILTER_THRESHOLD )
264+ else :
217265 cfg .CUSTOM_CLASSIFIER = custom_classifier # we treat this as absolute path, so no need to join with dirname
218266
219267 if custom_classifier .endswith (".tflite" ):
220268 cfg .LABELS_FILE = custom_classifier .replace (".tflite" , "_Labels.txt" ) # same for labels file
221269
222- if not os .path .isfile (cfg .LABELS_FILE ): # if the label file is not found, an old birdnet model might be used
270+ if not os .path .isfile (cfg .LABELS_FILE ): # if the label file is not found, an old birdnet model might be used
223271 cfg .LABELS_FILE = custom_classifier .replace ("Model_FP32.tflite" , "Labels.txt" )
224272
225- if not os .path .isfile (cfg .LABELS_FILE ): # if the label file is still not found, dont use labels
273+ if not os .path .isfile (cfg .LABELS_FILE ): # if the label file is still not found, dont use labels
226274 cfg .LABELS_FILE = None
227275 cfg .LABELS = None
228276 else :
229- cfg .LABELS = read_lines (cfg .LABELS_FILE )
277+ cfg .LABELS = read_lines (cfg .LABELS_FILE , fail_on_blank_lines = True )
230278 else :
231279 cfg .APPLY_SIGMOID = False
232280 # our output format
233281 cfg .LABELS_FILE = os .path .join (custom_classifier , "labels" , "label_names.csv" )
234282
235283 if not os .path .isfile (cfg .LABELS_FILE ):
236284 cfg .LABELS_FILE = os .path .join (custom_classifier , "assets" , "label.csv" )
237- cfg .LABELS = read_lines (cfg .LABELS_FILE )
238- else :
239- cfg .LABELS = [line .split ("," )[1 ] for line in read_lines (cfg .LABELS_FILE )]
240- else :
241- cfg .LATITUDE , cfg .LONGITUDE , cfg .WEEK = lat , lon , week
242- cfg .CUSTOM_CLASSIFIER = None
243-
244- if cfg .LATITUDE == - 1 and cfg .LONGITUDE == - 1 :
245- if not slist :
246- cfg .SPECIES_LIST_FILE = None
285+ cfg .LABELS = read_lines (cfg .LABELS_FILE , fail_on_blank_lines = True )
247286 else :
248- cfg .SPECIES_LIST_FILE = slist
249-
250- if os .path .isdir (cfg .SPECIES_LIST_FILE ):
251- cfg .SPECIES_LIST_FILE = os .path .join (cfg .SPECIES_LIST_FILE , "species_list.txt" )
252-
253- cfg .SPECIES_LIST = read_lines (cfg .SPECIES_LIST_FILE )
254- else :
255- cfg .SPECIES_LIST_FILE = None
256- cfg .SPECIES_LIST = get_species_list (cfg .LATITUDE , cfg .LONGITUDE , cfg .WEEK , cfg .LOCATION_FILTER_THRESHOLD )
287+ cfg .LABELS = [line .split ("," )[1 ] for line in read_lines (cfg .LABELS_FILE , fail_on_blank_lines = True )]
257288
258289 if cfg .LABELS_FILE :
259290 lfile = os .path .join (cfg .TRANSLATED_LABELS_PATH , os .path .basename (cfg .LABELS_FILE ).replace (".txt" , f"_{ locale } .txt" ))
0 commit comments