use super::builder::PredictorBuilderState;
use crate::TaskPredictorBuilder;
use crate::core::OcrResult;
use crate::core::errors::OCRError;
use crate::core::traits::OrtConfigurable;
use crate::core::traits::task::ImageTaskInput;
use crate::domain::adapters::{PPFormulaNetAdapterBuilder, UniMERNetAdapterBuilder};
use crate::domain::tasks::formula_recognition::{FormulaRecognitionConfig, FormulaRecognitionTask};
use crate::predictors::TaskPredictorCore;
use image::RgbImage;
use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FormulaModelKind {
UniMERNet,
PPFormulaNet,
}
impl FormulaModelKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::UniMERNet => "unimernet",
Self::PPFormulaNet => "pp_formulanet",
}
}
pub fn from_model_name(name: &str) -> Option<Self> {
match name {
"UniMERNet" => Some(FormulaModelKind::UniMERNet),
"PP-FormulaNet-S"
| "PP-FormulaNet-L"
| "PP-FormulaNet_plus-S"
| "PP-FormulaNet_plus-M"
| "PP-FormulaNet_plus-L" => Some(FormulaModelKind::PPFormulaNet),
_ => {
let name_lower = name.to_lowercase().replace('_', "-");
if name_lower.contains("unimernet") {
Some(FormulaModelKind::UniMERNet)
} else if name_lower.contains("pp-formulanet")
|| name_lower.contains("ppformulanet")
{
Some(FormulaModelKind::PPFormulaNet)
} else {
None
}
}
}
}
}
impl AsRef<str> for FormulaModelKind {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::str::FromStr for FormulaModelKind {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().replace('-', "_").as_str() {
"pp_formulanet" => Ok(Self::PPFormulaNet),
"unimernet" => Ok(Self::UniMERNet),
_ => Err(format!(
"unknown formula model kind {value:?}; expected 'pp_formulanet' or 'unimernet'"
)),
}
}
}
#[derive(Debug, Clone)]
pub struct FormulaRecognitionResult {
pub formulas: Vec<String>,
pub scores: Vec<Option<f32>>,
}
pub struct FormulaRecognitionPredictor {
core: TaskPredictorCore<FormulaRecognitionTask>,
}
impl FormulaRecognitionPredictor {
pub fn builder() -> FormulaRecognitionPredictorBuilder {
FormulaRecognitionPredictorBuilder::new()
}
pub fn predict(&self, images: Vec<RgbImage>) -> OcrResult<FormulaRecognitionResult> {
let input = ImageTaskInput::new(images);
let output = self.core.predict(input)?;
Ok(FormulaRecognitionResult {
formulas: output.formulas,
scores: output.scores,
})
}
}
#[derive(TaskPredictorBuilder)]
#[builder(config = FormulaRecognitionConfig)]
pub struct FormulaRecognitionPredictorBuilder {
state: PredictorBuilderState<FormulaRecognitionConfig>,
model_name: String,
tokenizer_path: Option<PathBuf>,
target_size: Option<(u32, u32)>,
model_kind: Option<FormulaModelKind>,
}
impl FormulaRecognitionPredictorBuilder {
pub fn new() -> Self {
Self {
state: PredictorBuilderState::new(FormulaRecognitionConfig {
score_threshold: 0.0,
max_length: 1536,
batch_size: 8,
}),
model_name: "UniMERNet".to_string(),
tokenizer_path: None,
target_size: None,
model_kind: None,
}
}
pub fn score_threshold(mut self, threshold: f32) -> Self {
self.state.config_mut().score_threshold = threshold;
self
}
pub fn max_length(mut self, max: usize) -> Self {
self.state.config_mut().max_length = max;
self
}
pub fn model_name(mut self, name: &str) -> Self {
self.model_name = name.to_string();
self
}
pub fn tokenizer_path<P: AsRef<Path>>(mut self, path: P) -> Self {
self.tokenizer_path = Some(path.as_ref().to_path_buf());
self
}
pub fn target_size(mut self, width: u32, height: u32) -> Self {
self.target_size = Some((width, height));
self
}
pub fn model_kind(mut self, kind: FormulaModelKind) -> Self {
self.model_kind = Some(kind);
self
}
pub fn build(
self,
model_source: impl Into<crate::core::ModelSource>,
) -> OcrResult<FormulaRecognitionPredictor> {
let Self {
state,
model_name,
tokenizer_path,
target_size,
model_kind,
} = self;
let (config, ort_config) = state.into_parts();
let tokenizer_path = tokenizer_path.ok_or_else(|| {
OCRError::missing_field("tokenizer_path", "FormulaRecognitionPredictor")
})?;
let tokenizer_path = super::resolve_asset_path(&tokenizer_path)?;
let model_kind = match model_kind {
Some(kind) => kind,
None => FormulaModelKind::from_model_name(&model_name).ok_or_else(|| {
OCRError::config_error_detailed(
"formula_recognition",
format!(
"Cannot infer formula model kind from '{model_name}'; call model_kind() with FormulaModelKind::PPFormulaNet or FormulaModelKind::UniMERNet"
),
)
})?,
};
let adapter = match model_kind {
FormulaModelKind::UniMERNet => {
let mut builder = UniMERNetAdapterBuilder::new()
.with_config(config.clone())
.model_name(&model_name)
.tokenizer_path(tokenizer_path);
if let Some((width, height)) = target_size {
builder = builder.target_size(width, height);
}
if let Some(ort_cfg) = ort_config.clone() {
builder = builder.with_ort_config(ort_cfg);
}
super::build_adapter(builder, model_source)?
}
FormulaModelKind::PPFormulaNet => {
let mut builder = PPFormulaNetAdapterBuilder::new()
.with_config(config.clone())
.model_name(&model_name)
.tokenizer_path(tokenizer_path);
if let Some((width, height)) = target_size {
builder = builder.target_size(width, height);
}
if let Some(ort_cfg) = ort_config.clone() {
builder = builder.with_ort_config(ort_cfg);
}
super::build_adapter(builder, model_source)?
}
};
Ok(FormulaRecognitionPredictor {
core: TaskPredictorCore::new(
adapter,
FormulaRecognitionTask::new(config.clone()),
config,
),
})
}
}
impl Default for FormulaRecognitionPredictorBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::{FormulaModelKind, FormulaRecognitionPredictorBuilder};
#[test]
fn formula_builder_defaults_to_unimernet() {
let builder = FormulaRecognitionPredictorBuilder::new();
assert_eq!(builder.model_name, "UniMERNet");
assert_eq!(builder.model_kind, None);
}
#[test]
fn formula_model_kind_inference_rejects_unknown_names() {
assert_eq!(
FormulaModelKind::from_model_name("UniMERNet"),
Some(FormulaModelKind::UniMERNet)
);
assert_eq!(
FormulaModelKind::from_model_name("PP-FormulaNet_plus-L"),
Some(FormulaModelKind::PPFormulaNet)
);
assert_eq!(
FormulaModelKind::from_model_name("pp_formulanet"),
Some(FormulaModelKind::PPFormulaNet)
);
assert_eq!(
FormulaModelKind::from_model_name("custom_unimernet_export"),
Some(FormulaModelKind::UniMERNet)
);
assert_eq!(FormulaModelKind::from_model_name("formula.onnx"), None);
assert_eq!("pp_formulanet".parse(), Ok(FormulaModelKind::PPFormulaNet));
assert_eq!("PP-FormulaNet".parse(), Ok(FormulaModelKind::PPFormulaNet));
assert!(
"custom_unimernet_export"
.parse::<FormulaModelKind>()
.is_err()
);
assert!("mystery".parse::<FormulaModelKind>().is_err());
}
}