use crate::error::{Error, Result};
use crate::types::{ModelConfig, ModelType};
pub fn detect_model_type(
input_shape: &[i64],
output_shapes: &[Vec<i64>],
override_type: Option<ModelType>,
) -> Result<ModelConfig> {
let sample_count = extract_sample_count(input_shape)?;
let num_outputs = output_shapes.len();
if let Some(model_type) = override_type {
return build_config_with_override(model_type, sample_count, output_shapes);
}
match (sample_count, num_outputs) {
(144_000, 1) => {
let num_species = extract_last_dim(&output_shapes[0])?;
Ok(ModelConfig {
model_type: ModelType::BirdNetV24,
sample_rate: 48_000,
segment_duration: 3.0,
sample_count: 144_000,
num_species,
embedding_dim: None,
})
}
(160_000, 2) => {
let embedding_dim = extract_last_dim(&output_shapes[0])?;
let num_species = extract_last_dim(&output_shapes[1])?;
Ok(ModelConfig {
model_type: ModelType::BirdNetV30,
sample_rate: 32_000,
segment_duration: 5.0,
sample_count: 160_000,
num_species,
embedding_dim: Some(embedding_dim),
})
}
(160_000, 4) => {
let embedding_dim = extract_last_dim(&output_shapes[0])?;
let num_species = extract_last_dim(&output_shapes[3])?;
Ok(ModelConfig {
model_type: ModelType::PerchV2,
sample_rate: 32_000,
segment_duration: 5.0,
sample_count: 160_000,
num_species,
embedding_dim: Some(embedding_dim),
})
}
_ => Err(Error::ModelDetection {
reason: format!(
"unsupported model: {sample_count} samples, {num_outputs} outputs \
(expected 144000/1, 160000/2, or 160000/4)"
),
}),
}
}
fn build_config_with_override(
model_type: ModelType,
sample_count: usize,
output_shapes: &[Vec<i64>],
) -> Result<ModelConfig> {
let expected_samples = model_type.sample_count();
if sample_count != expected_samples {
return Err(Error::ModelDetection {
reason: format!(
"model type {model_type:?} expects {expected_samples} samples, \
but model has {sample_count}"
),
});
}
let (embedding_dim, num_species) = match model_type {
ModelType::BirdNetV24 => {
if output_shapes.len() != 1 {
return Err(Error::ModelDetection {
reason: format!(
"`BirdNET` v2.4 expects 1 output, got {}",
output_shapes.len()
),
});
}
(None, extract_last_dim(&output_shapes[0])?)
}
ModelType::BirdNetV30 => {
if output_shapes.len() != 2 {
return Err(Error::ModelDetection {
reason: format!(
"`BirdNET` v3.0 expects 2 outputs, got {}",
output_shapes.len()
),
});
}
(
Some(extract_last_dim(&output_shapes[0])?),
extract_last_dim(&output_shapes[1])?,
)
}
ModelType::PerchV2 => {
if output_shapes.len() != 4 {
return Err(Error::ModelDetection {
reason: format!("`Perch` v2 expects 4 outputs, got {}", output_shapes.len()),
});
}
(
Some(extract_last_dim(&output_shapes[0])?),
extract_last_dim(&output_shapes[3])?, )
}
};
Ok(ModelConfig {
model_type,
sample_rate: model_type.sample_rate(),
segment_duration: model_type.segment_duration(),
sample_count,
num_species,
embedding_dim,
})
}
fn extract_sample_count(shape: &[i64]) -> Result<usize> {
let value = match shape.len() {
2 => shape[1],
3 => shape[2],
_ => {
return Err(Error::ModelDetection {
reason: format!("unexpected input shape: {shape:?}"),
});
}
};
usize::try_from(value).map_err(|_| Error::ModelDetection {
reason: format!("invalid sample count: {value}"),
})
}
fn extract_last_dim(shape: &[i64]) -> Result<usize> {
let value = shape.last().copied().ok_or_else(|| Error::ModelDetection {
reason: "empty output shape".to_string(),
})?;
usize::try_from(value).map_err(|_| Error::ModelDetection {
reason: format!("invalid dimension: {value}"),
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::disallowed_methods)]
#![allow(clippy::float_cmp)]
use super::*;
#[test]
fn test_detect_birdnet_v24() {
let input_shape = vec![1, 144_000];
let output_shapes = vec![vec![1, 6522]];
let config = detect_model_type(&input_shape, &output_shapes, None).unwrap();
assert_eq!(config.model_type, ModelType::BirdNetV24);
assert_eq!(config.sample_rate, 48_000);
assert_eq!(config.segment_duration, 3.0);
assert_eq!(config.sample_count, 144_000);
assert_eq!(config.num_species, 6522);
assert_eq!(config.embedding_dim, None);
}
#[test]
fn test_detect_birdnet_v30() {
let input_shape = vec![1, 160_000];
let output_shapes = vec![vec![1, 1024], vec![1, 1000]];
let config = detect_model_type(&input_shape, &output_shapes, None).unwrap();
assert_eq!(config.model_type, ModelType::BirdNetV30);
assert_eq!(config.sample_rate, 32_000);
assert_eq!(config.segment_duration, 5.0);
assert_eq!(config.sample_count, 160_000);
assert_eq!(config.num_species, 1000);
assert_eq!(config.embedding_dim, Some(1024));
}
#[test]
fn test_detect_perch_v2() {
let input_shape = vec![1, 160_000];
let output_shapes = vec![
vec![1, 1536], vec![1, 16, 4, 1536], vec![1, 500, 128], vec![1, 14795], ];
let config = detect_model_type(&input_shape, &output_shapes, None).unwrap();
assert_eq!(config.model_type, ModelType::PerchV2);
assert_eq!(config.sample_rate, 32_000);
assert_eq!(config.segment_duration, 5.0);
assert_eq!(config.sample_count, 160_000);
assert_eq!(config.num_species, 14795);
assert_eq!(config.embedding_dim, Some(1536));
}
#[test]
fn test_detect_with_perch_override() {
let input_shape = vec![1, 160_000];
let output_shapes = vec![
vec![1, 512], vec![1, 16, 4, 512], vec![1, 500, 128], vec![1, 500], ];
let config =
detect_model_type(&input_shape, &output_shapes, Some(ModelType::PerchV2)).unwrap();
assert_eq!(config.model_type, ModelType::PerchV2);
assert_eq!(config.embedding_dim, Some(512));
assert_eq!(config.num_species, 500);
}
#[test]
fn test_detect_with_invalid_override() {
let input_shape = vec![1, 160_000];
let output_shapes = vec![vec![1, 1024], vec![1, 1000]];
let result = detect_model_type(&input_shape, &output_shapes, Some(ModelType::BirdNetV24));
assert!(result.is_err());
}
#[test]
fn test_detect_unsupported_model() {
let input_shape = vec![1, 100_000]; let output_shapes = vec![vec![1, 1000]];
let result = detect_model_type(&input_shape, &output_shapes, None);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("unsupported model"));
}
#[test]
fn test_extract_sample_count_2d() {
assert_eq!(extract_sample_count(&[1, 144_000]).unwrap(), 144_000);
}
#[test]
fn test_extract_sample_count_3d() {
assert_eq!(extract_sample_count(&[1, 1, 144_000]).unwrap(), 144_000);
}
}