use crate::core::OCRError;
use crate::core::config::{OrtExecutionProvider, OrtGraphOptimizationLevel, OrtSessionConfig};
use crate::core::inference::{OrtInfer, TensorInput};
use crate::processors::{UniMERNetPreprocessParams, UniMERNetPreprocessor};
use image::RgbImage;
use ndarray::{ArrayBase, Axis, Data, Ix2};
#[derive(Debug, Clone)]
pub struct UniMERNetPreprocessConfig {
pub target_size: (u32, u32),
pub crop_threshold: u8,
pub padding_multiple: usize,
pub normalize_mean: [f32; 3],
pub normalize_std: [f32; 3],
}
impl Default for UniMERNetPreprocessConfig {
fn default() -> Self {
Self {
target_size: (672, 192), crop_threshold: 200,
padding_multiple: 32, normalize_mean: [0.7931, 0.7931, 0.7931],
normalize_std: [0.1738, 0.1738, 0.1738],
}
}
}
#[derive(Debug, Clone)]
pub struct UniMERNetPostprocessConfig {
pub sos_token_id: i64,
pub eos_token_id: i64,
pub vocab_size: i64,
}
impl Default for UniMERNetPostprocessConfig {
fn default() -> Self {
Self {
sos_token_id: 0,
eos_token_id: 2,
vocab_size: i64::MAX,
}
}
}
#[derive(Debug, Clone)]
pub struct UniMERNetModelOutput {
pub token_ids: ndarray::Array2<i64>,
}
#[derive(Debug)]
pub struct UniMERNetModel {
inference: OrtInfer,
preprocessor: UniMERNetPreprocessor,
_preprocess_config: UniMERNetPreprocessConfig,
}
impl UniMERNetModel {
pub fn new(
inference: OrtInfer,
preprocess_config: UniMERNetPreprocessConfig,
) -> Result<Self, OCRError> {
let params = UniMERNetPreprocessParams {
target_size: preprocess_config.target_size,
crop_threshold: preprocess_config.crop_threshold,
padding_multiple: preprocess_config.padding_multiple,
normalize_mean: preprocess_config.normalize_mean,
normalize_std: preprocess_config.normalize_std,
};
let preprocessor = UniMERNetPreprocessor::new(params);
Ok(Self {
inference,
preprocessor,
_preprocess_config: preprocess_config,
})
}
pub fn preprocess(&self, images: Vec<RgbImage>) -> Result<ndarray::Array4<f32>, OCRError> {
self.preprocessor.preprocess_batch(&images)
}
pub fn infer(
&self,
batch_tensor: &ndarray::Array4<f32>,
) -> Result<ndarray::Array2<i64>, OCRError> {
let input_name = self.inference.input_name();
let inputs = vec![(input_name, TensorInput::Array4(batch_tensor))];
let outputs = self
.inference
.infer(&inputs)
.map_err(|e| OCRError::Inference {
model_name: "UniMERNet".to_string(),
context: format!(
"failed to run inference on batch with shape {:?}",
batch_tensor.shape()
),
source: Box::new(e),
})?;
let output = outputs
.into_iter()
.next()
.ok_or_else(|| OCRError::InvalidInput {
message: "UniMERNet: no output returned from inference".to_string(),
})?;
output
.1
.try_into_array2_i64()
.map_err(|e| OCRError::Inference {
model_name: "UniMERNet".to_string(),
context: "failed to convert output to 2D i64 array".to_string(),
source: Box::new(e),
})
}
pub fn postprocess(
&self,
token_ids: ndarray::Array2<i64>,
_config: &UniMERNetPostprocessConfig,
) -> Result<UniMERNetModelOutput, OCRError> {
Ok(UniMERNetModelOutput { token_ids })
}
pub fn forward(
&self,
images: Vec<RgbImage>,
config: &UniMERNetPostprocessConfig,
) -> Result<UniMERNetModelOutput, OCRError> {
let batch_tensor = self.preprocess(images)?;
let token_ids = self.infer(&batch_tensor)?;
let output = self.postprocess(token_ids, config)?;
Ok(output)
}
pub fn filter_tokens<D>(
token_ids: &ArrayBase<D, Ix2>,
config: &UniMERNetPostprocessConfig,
) -> Vec<Vec<u32>>
where
D: Data<Elem = i64>,
{
let batch_size = token_ids.shape()[0];
let mut filtered_tokens = Vec::with_capacity(batch_size);
for batch_idx in 0..batch_size {
let row = token_ids.index_axis(Axis(0), batch_idx);
let tokens: Vec<u32> = row
.iter()
.copied()
.take_while(|&id| id != config.eos_token_id)
.take_while(|&id| id < 0 || id < config.vocab_size)
.filter(|&id| id >= 0 && id != config.sos_token_id)
.map(|id| id as u32)
.collect();
filtered_tokens.push(tokens);
}
filtered_tokens
}
}
#[derive(Debug, Default)]
pub struct UniMERNetModelBuilder {
preprocess_config: Option<UniMERNetPreprocessConfig>,
ort_config: Option<crate::core::config::OrtSessionConfig>,
}
impl UniMERNetModelBuilder {
pub fn new() -> Self {
Self {
preprocess_config: None,
ort_config: None,
}
}
pub fn preprocess_config(mut self, config: UniMERNetPreprocessConfig) -> Self {
self.preprocess_config = Some(config);
self
}
pub fn target_size(mut self, width: u32, height: u32) -> Self {
let mut config = self.preprocess_config.unwrap_or_default();
config.target_size = (width, height);
self.preprocess_config = Some(config);
self
}
pub fn padding_multiple(mut self, multiple: usize) -> Self {
let mut config = self.preprocess_config.unwrap_or_default();
config.padding_multiple = multiple;
self.preprocess_config = Some(config);
self
}
pub fn with_ort_config(mut self, config: crate::core::config::OrtSessionConfig) -> Self {
self.ort_config = Some(config);
self
}
pub fn build(self, model_path: &std::path::Path) -> Result<UniMERNetModel, OCRError> {
let ort_config = self.ort_config.map(Self::configure_unimernet_ort_for_cuda);
let inference = if ort_config.is_some() {
use crate::core::config::ModelInferenceConfig;
let common_config = ModelInferenceConfig {
ort_session: ort_config,
model_name: Some("UniMERNet".to_string()),
..Default::default()
};
OrtInfer::from_config(&common_config, model_path, None)?
} else {
OrtInfer::new(model_path, None)?
};
let mut preprocess_config = self.preprocess_config.unwrap_or_default();
if preprocess_config.target_size == (672, 192)
&& let Some(shape) = inference.primary_input_shape()
&& shape.len() >= 4
{
let height = shape[shape.len() - 2];
let width = shape[shape.len() - 1];
if height > 0 && width > 0 {
preprocess_config.target_size = (width as u32, height as u32);
}
}
UniMERNetModel::new(inference, preprocess_config)
}
fn configure_unimernet_ort_for_cuda(mut config: OrtSessionConfig) -> OrtSessionConfig {
if !Self::uses_cuda(&config) {
return config;
}
config.optimization_level = Some(OrtGraphOptimizationLevel::Level1);
if config.enable_mem_pattern.is_none() {
config.enable_mem_pattern = Some(false);
}
let entries = config
.session_config_entries
.get_or_insert_with(Default::default);
let disabled_optimizers = entries
.entry("optimization.disable_specified_optimizers".to_string())
.or_default();
if !disabled_optimizers
.split(',')
.any(|name| name.trim() == "ConstantFolding")
{
if !disabled_optimizers.trim().is_empty() {
disabled_optimizers.push(',');
}
disabled_optimizers.push_str("ConstantFolding");
}
config
}
fn uses_cuda(config: &OrtSessionConfig) -> bool {
config
.execution_providers
.as_ref()
.is_some_and(|providers| {
providers
.iter()
.any(|provider| matches!(provider, OrtExecutionProvider::CUDA { .. }))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::arr2;
#[test]
fn filter_tokens_stops_at_vocab_sentinel() {
let token_ids = arr2(&[[0, 42, 49_999, 4_096_990_134i64, 77, 2]]);
let config = UniMERNetPostprocessConfig {
sos_token_id: 0,
eos_token_id: 2,
vocab_size: 50_000,
};
let filtered = UniMERNetModel::filter_tokens(&token_ids, &config);
assert_eq!(filtered, vec![vec![42, 49_999]]);
}
#[test]
fn filter_tokens_still_stops_at_eos() {
let token_ids = arr2(&[[0, 42, 2, 43]]);
let config = UniMERNetPostprocessConfig {
sos_token_id: 0,
eos_token_id: 2,
vocab_size: 50_000,
};
let filtered = UniMERNetModel::filter_tokens(&token_ids, &config);
assert_eq!(filtered, vec![vec![42]]);
}
#[test]
fn cuda_config_disables_constant_folding_for_unimernet() {
let config = OrtSessionConfig::new().with_execution_providers(vec![
OrtExecutionProvider::CUDA {
device_id: Some(0),
gpu_mem_limit: None,
arena_extend_strategy: None,
cudnn_conv_algo_search: None,
cudnn_conv_use_max_workspace: None,
},
OrtExecutionProvider::CPU,
]);
let configured = UniMERNetModelBuilder::configure_unimernet_ort_for_cuda(config);
assert!(matches!(
configured.optimization_level,
Some(OrtGraphOptimizationLevel::Level1)
));
assert_eq!(configured.enable_mem_pattern, Some(false));
assert_eq!(
configured
.session_config_entries
.as_ref()
.and_then(|entries| entries.get("optimization.disable_specified_optimizers"))
.map(String::as_str),
Some("ConstantFolding")
);
}
#[test]
fn cpu_config_keeps_unimernet_ort_config_unchanged() {
let config =
OrtSessionConfig::new().with_optimization_level(OrtGraphOptimizationLevel::All);
let configured = UniMERNetModelBuilder::configure_unimernet_ort_for_cuda(config);
assert!(matches!(
configured.optimization_level,
Some(OrtGraphOptimizationLevel::All)
));
assert!(configured.session_config_entries.is_none());
}
}