#[cfg(feature = "burn")]
use crate::burn_backend::tdt::{TdtDecoderJoint, TdtEncoder};
use crate::error::{Error, Result};
use crate::execution::ModelConfig as ExecutionConfig;
#[cfg(feature = "ort")]
use ndarray::Array1;
use ndarray::{Array2, Array3};
#[cfg(feature = "ort")]
use ort::session::Session;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct TDTModelConfig {
pub vocab_size: usize,
}
impl TDTModelConfig {
pub fn new(vocab_size: usize) -> Self {
Self { vocab_size }
}
}
pub struct ParakeetTDTModel {
encoder: Encoder,
decoder_joint: DecoderJoint,
config: TDTModelConfig,
}
enum Encoder {
#[cfg(feature = "ort")]
Ort(Session),
#[cfg(feature = "burn")]
Burn(Box<TdtEncoder>),
}
enum DecoderJoint {
#[cfg(feature = "ort")]
Ort(Session),
#[cfg(feature = "burn")]
Burn(Box<TdtDecoderJoint>),
}
impl Encoder {
fn load(path: &Path, config: &ExecutionConfig) -> Result<Self> {
#[cfg(feature = "burn")]
if config.execution_provider.is_burn() {
return Ok(Self::Burn(Box::new(TdtEncoder::load(
path,
config.execution_provider,
)?)));
}
#[cfg(feature = "ort")]
{
Ok(Self::Ort(config.build_session(path)?))
}
#[cfg(not(feature = "ort"))]
{
Err(Error::Config(format!(
"{:?} needs the `ort` feature",
config.execution_provider
)))
}
}
}
impl DecoderJoint {
#[cfg_attr(not(feature = "burn"), allow(unused_variables))]
fn load(path: &Path, config: &ExecutionConfig, vocab_size: usize) -> Result<Self> {
#[cfg(feature = "burn")]
if config.execution_provider.is_burn() {
return Ok(Self::Burn(Box::new(TdtDecoderJoint::load(
path,
config.execution_provider,
vocab_size,
)?)));
}
#[cfg(feature = "ort")]
{
Ok(Self::Ort(config.build_session(path)?))
}
#[cfg(not(feature = "ort"))]
{
Err(Error::Config(format!(
"{:?} needs the `ort` feature",
config.execution_provider
)))
}
}
}
impl ParakeetTDTModel {
pub fn from_pretrained_with_configs<P: AsRef<Path>>(
model_dir: P,
exec_config: ExecutionConfig,
joint_config: ExecutionConfig,
vocab_size: usize,
) -> Result<Self> {
let model_dir = model_dir.as_ref();
if vocab_size == 0 {
return Err(Error::Config("TDT vocabulary is empty".into()));
}
let encoder_int8 = !exec_config.execution_provider.is_burn();
let joint_int8 = !joint_config.execution_provider.is_burn();
let encoder_path = Self::find_encoder(model_dir, encoder_int8)?;
let decoder_joint_path = Self::find_decoder_joint(model_dir, joint_int8)?;
let config = TDTModelConfig::new(vocab_size);
let encoder = Encoder::load(&encoder_path, &exec_config)?;
let decoder_joint = DecoderJoint::load(&decoder_joint_path, &joint_config, vocab_size)?;
Ok(Self {
encoder,
decoder_joint,
config,
})
}
fn find_encoder(dir: &Path, int8: bool) -> Result<PathBuf> {
let candidates = [
"encoder-model.onnx",
"encoder.onnx",
"encoder-model.int8.onnx",
];
for candidate in &candidates {
let path = dir.join(candidate);
if path.exists() && (int8 || !candidate.contains(".int8.")) {
return Ok(path);
}
}
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(name) = path.file_name().and_then(|s| s.to_str())
&& name.starts_with("encoder")
&& name.ends_with(".onnx")
&& (int8 || !name.contains(".int8."))
{
return Ok(path);
}
}
}
Err(Error::Config(format!(
"No encoder model found in {}",
dir.display()
)))
}
fn find_decoder_joint(dir: &Path, int8: bool) -> Result<PathBuf> {
let candidates = [
"decoder_joint-model.onnx",
"decoder_joint-model.int8.onnx",
"decoder_joint.onnx",
"decoder-model.onnx",
];
for candidate in &candidates {
let path = dir.join(candidate);
if path.exists() && (int8 || !candidate.contains(".int8.")) {
return Ok(path);
}
}
Err(Error::Config(format!(
"No decoder_joint model found in {}",
dir.display()
)))
}
pub fn forward(
&mut self,
features: Array2<f32>,
) -> Result<(Vec<usize>, Vec<usize>, Vec<usize>)> {
let (encoder_out, encoder_len) = self.run_encoder(&features)?;
let (tokens, frame_indices, durations) = self.greedy_decode(&encoder_out, encoder_len)?;
Ok((tokens, frame_indices, durations))
}
fn run_encoder(&mut self, features: &Array2<f32>) -> Result<(Array3<f32>, i64)> {
let batch_size = 1;
let time_steps = features.shape()[0];
let feature_size = features.shape()[1];
let input = features
.t()
.to_shape((batch_size, feature_size, time_steps))
.map_err(|e| Error::Model(format!("Failed to reshape encoder input: {e}")))?
.to_owned();
match &mut self.encoder {
#[cfg(feature = "ort")]
Encoder::Ort(session) => {
let input_length = Array1::from_vec(vec![time_steps as i64]);
let input_value = ort::value::Value::from_array(input)?;
let length_value = ort::value::Value::from_array(input_length)?;
let outputs = session.run(ort::inputs!(
"audio_signal" => input_value,
"length" => length_value
))?;
let encoder_array =
crate::tensor_utils::extract_3d_f32(&outputs["outputs"], "encoder output")?;
let encoded_len = crate::tensor_utils::extract_scalar_i64(
&outputs["encoded_lengths"],
"encoder lengths",
)?;
Ok((encoder_array, encoded_len))
}
#[cfg(feature = "burn")]
Encoder::Burn(encoder) => encoder.run(input),
}
}
fn joint_step(
&mut self,
frame: Array3<f32>,
last_token: i32,
state_h: &mut Array3<f32>,
state_c: &mut Array3<f32>,
) -> Result<(usize, usize)> {
let vocab_size = self.config.vocab_size;
let blank_id = vocab_size - 1;
match &mut self.decoder_joint {
#[cfg(feature = "ort")]
DecoderJoint::Ort(session) => {
let targets = Array2::from_shape_vec((1, 1), vec![last_token])
.map_err(|e| Error::Model(format!("Failed to create targets: {e}")))?;
let outputs = session.run(ort::inputs!(
"encoder_outputs" => ort::value::Value::from_array(frame)?,
"targets" => ort::value::Value::from_array(targets)?,
"target_length" => ort::value::Value::from_array(Array1::from_vec(vec![1i32]))?,
"input_states_1" => ort::value::Value::from_array(state_h.clone())?,
"input_states_2" => ort::value::Value::from_array(state_c.clone())?
))?;
let (_, logits_data) = outputs["outputs"]
.try_extract_tensor::<f32>()
.map_err(|e| Error::Model(format!("Failed to extract logits: {e}")))?;
let (token_id, duration_step) = pick(logits_data, vocab_size);
if token_id != blank_id {
if let Ok((h_shape, h_data)) =
outputs["output_states_1"].try_extract_tensor::<f32>()
{
let dims = h_shape.as_ref();
*state_h = Array3::from_shape_vec(
(dims[0] as usize, dims[1] as usize, dims[2] as usize),
h_data.to_vec(),
)
.map_err(|e| Error::Model(format!("Failed to update state_h: {e}")))?;
}
if let Ok((c_shape, c_data)) =
outputs["output_states_2"].try_extract_tensor::<f32>()
{
let dims = c_shape.as_ref();
*state_c = Array3::from_shape_vec(
(dims[0] as usize, dims[1] as usize, dims[2] as usize),
c_data.to_vec(),
)
.map_err(|e| Error::Model(format!("Failed to update state_c: {e}")))?;
}
}
Ok((token_id, duration_step))
}
#[cfg(feature = "burn")]
DecoderJoint::Burn(decoder) => {
let step = decoder.step(frame.view(), last_token, state_h, state_c)?;
let (token_id, duration_step) = pick(&step.logits, vocab_size);
if token_id != blank_id {
*state_h = step.state_h;
*state_c = step.state_c;
}
Ok((token_id, duration_step))
}
}
}
fn greedy_decode(
&mut self,
encoder_out: &Array3<f32>,
_encoder_len: i64,
) -> Result<(Vec<usize>, Vec<usize>, Vec<usize>)> {
let encoder_dim = encoder_out.shape()[1];
let time_steps = encoder_out.shape()[2];
let vocab_size = self.config.vocab_size;
let max_tokens_per_step = 10;
let blank_id = vocab_size - 1;
let mut state_h = Array3::<f32>::zeros((2, 1, 640));
let mut state_c = Array3::<f32>::zeros((2, 1, 640));
let mut tokens = Vec::new();
let mut frame_indices = Vec::new();
let mut durations = Vec::new();
let mut t = 0;
let mut emitted_tokens = 0;
let mut last_emitted_token = blank_id as i32;
while t < time_steps {
let frame = encoder_out.slice(ndarray::s![0, .., t]).to_owned();
let frame_reshaped = frame
.to_shape((1, encoder_dim, 1))
.map_err(|e| Error::Model(format!("Failed to reshape frame: {e}")))?
.to_owned();
let (token_id, duration_step) = self.joint_step(
frame_reshaped,
last_emitted_token,
&mut state_h,
&mut state_c,
)?;
if token_id != blank_id {
tokens.push(token_id);
frame_indices.push(t);
durations.push(duration_step);
last_emitted_token = token_id as i32;
emitted_tokens += 1;
}
if duration_step > 0 {
t += duration_step;
emitted_tokens = 0;
} else if token_id == blank_id || emitted_tokens >= max_tokens_per_step {
t += 1;
emitted_tokens = 0;
}
}
Ok((tokens, frame_indices, durations))
}
}
fn pick(logits: &[f32], vocab_size: usize) -> (usize, usize) {
let argmax = |values: &[f32]| {
values
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
};
let (vocab_logits, duration_logits) = logits.split_at(vocab_size.min(logits.len()));
let token_id = argmax(vocab_logits).unwrap_or(vocab_size - 1);
let duration_step = argmax(duration_logits).unwrap_or(0);
(token_id, duration_step)
}
#[cfg(test)]
mod tests {
use super::pick;
#[test]
fn pick_splits_token_and_duration_logits() {
assert_eq!(pick(&[0.1, 0.9, 0.2, 0.3, 0.7], 3), (1, 1));
assert_eq!(pick(&[0.1, 0.2, 0.9], 3), (2, 0));
}
}