use std::path::Path;
use ort::value::TensorElementType;
use crate::error::{RapidOcrError, Result};
pub const DYNAMIC_DIM: i64 = -1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TensorSpec {
pub name: String,
pub rank: usize,
pub dims: Vec<i64>,
pub dimension_symbols: Vec<Option<String>>,
pub element_type: TensorElementType,
}
impl TensorSpec {
pub fn new(name: impl Into<String>, dims: Vec<i64>, element_type: TensorElementType) -> Self {
let rank = dims.len();
Self {
name: name.into(),
rank,
dims,
dimension_symbols: vec![None; rank],
element_type,
}
}
}
#[derive(Debug, Clone)]
pub struct ModelIoProbe {
pub inputs: Vec<TensorSpec>,
pub outputs: Vec<TensorSpec>,
}
pub fn require_single_input<'a>(
probe: &'a ModelIoProbe,
model_path: &Path,
model_kind: &str,
) -> Result<&'a TensorSpec> {
if probe.inputs.len() != 1 {
return Err(RapidOcrError::Config(format!(
"{model_kind} model must expose exactly one input, got {} (model={})",
probe.inputs.len(),
model_path.display()
)));
}
Ok(&probe.inputs[0])
}
pub fn require_single_output<'a>(
probe: &'a ModelIoProbe,
model_path: &Path,
model_kind: &str,
) -> Result<&'a TensorSpec> {
if probe.outputs.len() != 1 {
return Err(RapidOcrError::Config(format!(
"{model_kind} model must expose exactly one output, got {} (model={})",
probe.outputs.len(),
model_path.display()
)));
}
Ok(&probe.outputs[0])
}
pub fn require_outputs<'a>(
probe: &'a ModelIoProbe,
model_path: &Path,
model_kind: &str,
expected: &[&str],
) -> Result<Vec<&'a TensorSpec>> {
let actual = probe
.outputs
.iter()
.map(|spec| spec.name.as_str())
.collect::<Vec<_>>()
.join(", ");
let mut resolved = Vec::with_capacity(expected.len());
for name in expected {
match probe.outputs.iter().find(|spec| spec.name == *name) {
Some(spec) => resolved.push(spec),
None => {
return Err(RapidOcrError::Config(format!(
"{model_kind} model output mismatch: expected [{}], got {} outputs [{}] (missing `{name}`) (model={})",
expected.join(", "),
probe.outputs.len(),
actual,
model_path.display()
)));
}
}
}
let unexpected = probe
.outputs
.iter()
.map(|spec| spec.name.as_str())
.filter(|name| !expected.contains(name))
.collect::<Vec<_>>();
if !unexpected.is_empty() {
return Err(RapidOcrError::Config(format!(
"{model_kind} model output mismatch: expected [{}], got {} outputs [{}] (unexpected [{}]) (model={})",
expected.join(", "),
probe.outputs.len(),
actual,
unexpected.join(", "),
model_path.display()
)));
}
Ok(resolved)
}
pub fn require_tensor(
spec: &TensorSpec,
io_kind: &str,
expected_rank: usize,
expected_type: TensorElementType,
model_path: &Path,
) -> Result<()> {
if spec.rank != expected_rank {
return Err(RapidOcrError::Config(format!(
"model {io_kind} `{}` rank mismatch: expected {expected_rank}, got {} (model={})",
spec.name,
spec.rank,
model_path.display()
)));
}
if spec.element_type != expected_type {
return Err(RapidOcrError::Config(format!(
"model {io_kind} `{}` dtype mismatch: expected {}, got {} (model={})",
spec.name,
element_type_label(expected_type),
element_type_label(spec.element_type),
model_path.display()
)));
}
Ok(())
}
fn element_type_label(element_type: TensorElementType) -> String {
match element_type {
TensorElementType::Float32 => "FLOAT32".to_string(),
TensorElementType::Float16 => "FLOAT16".to_string(),
TensorElementType::Int64 => "INT64".to_string(),
TensorElementType::Int32 => "INT32".to_string(),
TensorElementType::Uint8 => "UINT8".to_string(),
TensorElementType::Int8 => "INT8".to_string(),
other => format!("{other:?}"),
}
}
pub fn require_fixed_dim(
spec: &TensorSpec,
io_kind: &str,
dim_index: usize,
expected: i64,
model_path: &Path,
) -> Result<()> {
let Some(actual) = spec.dims.get(dim_index) else {
return Err(RapidOcrError::Config(format!(
"model {io_kind} `{}` is missing dim index {dim_index} (dims={:?}, model={})",
spec.name,
spec.dims,
model_path.display()
)));
};
if *actual != expected {
return Err(RapidOcrError::Config(format!(
"model {io_kind} `{}` dim mismatch at index {dim_index}: expected {expected}, got {actual} (dims={:?}, model={})",
spec.name,
spec.dims,
model_path.display()
)));
}
Ok(())
}
pub fn require_dynamic_dim_symbol<'a>(
spec: &'a TensorSpec,
io_kind: &str,
dim_index: usize,
model_path: &Path,
) -> Result<&'a str> {
let Some(actual) = spec.dims.get(dim_index) else {
return Err(RapidOcrError::Config(format!(
"model {io_kind} `{}` is missing dim index {dim_index} (dims={:?}, model={})",
spec.name,
spec.dims,
model_path.display()
)));
};
if *actual != DYNAMIC_DIM {
return Err(RapidOcrError::Config(format!(
"model {io_kind} `{}` dim {dim_index} is already fixed to {actual}, so it cannot be overridden (dims={:?}, model={})",
spec.name,
spec.dims,
model_path.display()
)));
}
match spec
.dimension_symbols
.get(dim_index)
.and_then(Option::as_deref)
{
Some(symbol) => Ok(symbol),
None => Err(RapidOcrError::Config(format!(
"model {io_kind} `{}` dim {dim_index} is dynamic but carries no `dim_param` symbol, and ONNX Runtime can only override a dimension by symbol name (dims={:?}, symbols={:?}, model={})",
spec.name,
spec.dims,
spec.dimension_symbols,
model_path.display()
))),
}
}