use ndarray::{Array1, Array2, Array3};
use ort::session::Session;
use ort::value::Value;
use std::path::Path;
use std::sync::Mutex;
use crate::traits::AsrError;
pub const ENCODER_INPUT_AUDIO: &str = "audio_signal";
pub const ENCODER_INPUT_LENGTH: &str = "length";
pub const ENCODER_OUTPUT_EMBEDDINGS: &str = "encoder_embeddings";
pub const ENCODER_OUTPUT_MASK: &str = "encoder_mask";
#[derive(Debug)]
pub struct EncoderOutput {
pub embeddings: Array3<f32>,
pub mask: Array2<i64>,
}
pub fn pack_mel_for_encoder(
mel_row_major: &[f32],
n_mels: usize,
n_frames: usize,
) -> Result<(Array3<f32>, Array1<i64>), AsrError> {
if n_mels == 0 || n_frames == 0 {
return Err(AsrError::Inference(format!("empty mel features (n_mels={n_mels}, n_frames={n_frames})")));
}
let expected = n_mels * n_frames;
if mel_row_major.len() != expected {
return Err(AsrError::Inference(format!(
"mel buffer length {} does not match n_mels({}) * n_frames({}) = {}",
mel_row_major.len(),
n_mels,
n_frames,
expected
)));
}
let mut packed = Array3::<f32>::zeros((1, n_mels, n_frames));
for t in 0..n_frames {
for m in 0..n_mels {
packed[[0, m, t]] = mel_row_major[t * n_mels + m];
}
}
let length: Array1<i64> = Array1::from(vec![n_frames as i64]);
Ok((packed, length))
}
pub struct CanaryEncoder {
session: Mutex<Session>,
profiling: bool,
}
impl CanaryEncoder {
pub fn load(path: impl AsRef<Path>) -> Result<Self, AsrError> {
let path = path.as_ref();
let builder = Session::builder().map_err(|e| AsrError::ModelLoad(format!("Canary encoder builder {}: {e}", path.display())))?;
let (mut builder, profiling) = crate::canary::profiling::apply(builder, "encoder")
.map_err(|e| AsrError::Inference(format!("Canary encoder profiling {}: {e}", path.display())))?;
let session = builder.commit_from_file(path).map_err(|e| AsrError::ModelLoad(format!("load Canary encoder {}: {e}", path.display())))?;
validate_encoder_io(&session, path)?;
Ok(Self {
session: Mutex::new(session),
profiling,
})
}
pub fn encode(
&self,
mel_row_major: &[f32],
n_mels: usize,
n_frames: usize,
) -> Result<EncoderOutput, AsrError> {
let (audio_signal, length) = pack_mel_for_encoder(mel_row_major, n_mels, n_frames)?;
let audio_value = Value::from_array(audio_signal).map_err(|e| AsrError::Inference(format!("audio_signal Value: {e}")))?;
let length_value = Value::from_array(length).map_err(|e| AsrError::Inference(format!("length Value: {e}")))?;
let mut session = self.session.lock().map_err(|e| AsrError::Inference(format!("encoder session lock poisoned: {e}")))?;
let outputs = session
.run(vec![
(ENCODER_INPUT_AUDIO, audio_value.into_dyn()),
(ENCODER_INPUT_LENGTH, length_value.into_dyn()),
])
.map_err(|e| AsrError::Inference(format!("Canary encoder run: {e}")))?;
let emb_idx =
output_index(&outputs, ENCODER_OUTPUT_EMBEDDINGS).ok_or_else(|| AsrError::Inference(format!("encoder missing output {ENCODER_OUTPUT_EMBEDDINGS}")))?;
let mask_idx = output_index(&outputs, ENCODER_OUTPUT_MASK).ok_or_else(|| AsrError::Inference(format!("encoder missing output {ENCODER_OUTPUT_MASK}")))?;
let embeddings = outputs[emb_idx]
.try_extract_array::<f32>()
.map_err(|e| AsrError::Inference(format!("extract {ENCODER_OUTPUT_EMBEDDINGS}: {e}")))?
.to_owned()
.into_dimensionality::<ndarray::Ix3>()
.map_err(|e| AsrError::Inference(format!("{ENCODER_OUTPUT_EMBEDDINGS} rank: {e}")))?;
let mask = outputs[mask_idx]
.try_extract_array::<i64>()
.map_err(|e| AsrError::Inference(format!("extract {ENCODER_OUTPUT_MASK}: {e}")))?
.to_owned()
.into_dimensionality::<ndarray::Ix2>()
.map_err(|e| AsrError::Inference(format!("{ENCODER_OUTPUT_MASK} rank: {e}")))?;
Ok(EncoderOutput { embeddings, mask })
}
}
impl Drop for CanaryEncoder {
fn drop(&mut self) {
if self.profiling {
crate::canary::profiling::flush(&self.session, "encoder");
}
}
}
fn validate_encoder_io(session: &Session, path: &Path) -> Result<(), AsrError> {
let input_names: Vec<String> = session
.inputs()
.iter()
.map(|i| i.name().to_string())
.collect();
if !input_names.iter().any(|n| n == ENCODER_INPUT_AUDIO) {
return Err(AsrError::Inference(format!(
"encoder {} missing input {ENCODER_INPUT_AUDIO} (have: {input_names:?})",
path.display()
)));
}
if !input_names.iter().any(|n| n == ENCODER_INPUT_LENGTH) {
return Err(AsrError::Inference(format!(
"encoder {} missing input {ENCODER_INPUT_LENGTH} (have: {input_names:?})",
path.display()
)));
}
let output_names: Vec<String> = session
.outputs()
.iter()
.map(|o| o.name().to_string())
.collect();
if !output_names.iter().any(|n| n == ENCODER_OUTPUT_EMBEDDINGS) {
return Err(AsrError::Inference(format!(
"encoder {} missing output {ENCODER_OUTPUT_EMBEDDINGS} (have: {output_names:?})",
path.display()
)));
}
if !output_names.iter().any(|n| n == ENCODER_OUTPUT_MASK) {
return Err(AsrError::Inference(format!(
"encoder {} missing output {ENCODER_OUTPUT_MASK} (have: {output_names:?})",
path.display()
)));
}
Ok(())
}
fn output_index(outputs: &ort::session::SessionOutputs<'_>, name: &str) -> Option<usize> {
outputs.keys().position(|k| k == name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pack_mel_transposes_to_n_mels_t_layout() {
let mel = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, ];
let (packed, length) = pack_mel_for_encoder(&mel, 3, 4).unwrap();
assert_eq!(packed.dim(), (1, 3, 4));
assert_eq!(packed[[0, 0, 0]], 1.0);
assert_eq!(packed[[0, 1, 0]], 2.0);
assert_eq!(packed[[0, 2, 0]], 3.0);
assert_eq!(packed[[0, 0, 3]], 10.0);
assert_eq!(packed[[0, 1, 3]], 11.0);
assert_eq!(packed[[0, 2, 3]], 12.0);
assert_eq!(packed[[0, 1, 2]], 8.0);
assert_eq!(length.shape(), &[1]);
assert_eq!(length[0], 4);
}
#[test]
fn pack_mel_rejects_size_mismatch() {
let mel = vec![0.0_f32; 11];
let err = pack_mel_for_encoder(&mel, 3, 4).unwrap_err();
assert!(err.to_string().contains("does not match"), "{}", err);
}
#[test]
fn pack_mel_rejects_zero_dimensions() {
assert!(pack_mel_for_encoder(&[1.0], 0, 1).is_err());
assert!(pack_mel_for_encoder(&[1.0], 1, 0).is_err());
assert!(pack_mel_for_encoder(&[], 0, 0).is_err());
}
#[test]
fn pack_mel_canary_default_shape() {
let n_frames = 98;
let n_mels = 128;
let mel = vec![0.0_f32; n_frames * n_mels];
let (packed, length) = pack_mel_for_encoder(&mel, n_mels, n_frames).unwrap();
assert_eq!(packed.dim(), (1, 128, 98));
assert_eq!(length[0], 98);
}
#[test]
fn load_nonexistent_model_returns_error() {
match CanaryEncoder::load("/nonexistent/path/to/encoder.onnx") {
Ok(_) => panic!("expected error, got Ok"),
Err(e) => assert!(e.to_string().contains("load Canary encoder"), "{}", e.to_string()),
}
}
#[test]
fn io_name_constants_match_onnx_asr_conventions() {
assert_eq!(ENCODER_INPUT_AUDIO, "audio_signal");
assert_eq!(ENCODER_INPUT_LENGTH, "length");
assert_eq!(ENCODER_OUTPUT_EMBEDDINGS, "encoder_embeddings");
assert_eq!(ENCODER_OUTPUT_MASK, "encoder_mask");
}
}