1use crate::ocr_error::OcrError;
43use ndarray::{Array, Array4};
44use ort::{
45 inputs,
46 session::{builder::GraphOptimizationLevel, Session},
47 value::Tensor,
48};
49use std::path::Path;
50
51pub const FORMULA_INPUT_SIZE: u32 = 768;
53
54const BOS_ID: i64 = 0;
56const PAD_ID: i64 = 1;
57const EOS_ID: i64 = 2;
58
59const MAX_NEW_TOKENS: usize = 2560;
61
62#[derive(Debug, Clone)]
66pub struct FormulaResult {
67 pub latex: String,
70 pub score: f32,
73}
74
75struct Tokenizer {
78 id_to_token: Vec<String>,
80}
81
82impl Tokenizer {
83 fn from_json(path: &Path) -> Result<Self, OcrError> {
87 let content = std::fs::read_to_string(path)
88 .map_err(|e| OcrError::ModelHubError(format!("tokenizer.json: {e}")))?;
89 let json: serde_json::Value = serde_json::from_str(&content)
90 .map_err(|e| OcrError::ModelHubError(format!("parse tokenizer.json: {e}")))?;
91
92 let vocab = json.get("model")
96 .and_then(|m| m.get("vocab"))
97 .or_else(|| json.get("vocab"))
98 .and_then(|v| v.as_object())
99 .ok_or_else(|| OcrError::ModelHubError(
100 "tokenizer.json: campo 'model.vocab' non trovato".into()
101 ))?;
102
103 let max_id = vocab.values()
105 .filter_map(|v| v.as_i64())
106 .max()
107 .unwrap_or(0) as usize;
108
109 let mut id_to_token = vec![String::new(); max_id + 1];
110 for (token, id_val) in vocab {
111 if let Some(id) = id_val.as_i64() {
112 let id = id as usize;
113 if id < id_to_token.len() {
114 id_to_token[id] = token.clone();
115 }
116 }
117 }
118
119 eprintln!("[FormulaRec] tokenizer caricato: {} token", id_to_token.len());
120 Ok(Self { id_to_token })
121 }
122
123 fn fallback() -> Self {
127 let mut id_to_token = vec![String::new(); 50_000];
128 id_to_token[BOS_ID as usize] = "<s>".to_string();
129 id_to_token[PAD_ID as usize] = "<pad>".to_string();
130 id_to_token[EOS_ID as usize] = "</s>".to_string();
131 eprintln!(
132 "[FormulaRec] WARN: tokenizer.json non disponibile — \
133 output sarà token IDs testuale. Scarica il modello con \
134 ModelHub::ensure_single(PpStructureModel::FormulaRec)."
135 );
136 Self { id_to_token }
137 }
138
139 fn decode(&self, ids: &[i64]) -> String {
140 let mut out = String::new();
141 let mut prev_needs_space = false;
142
143 for &id in ids {
144 if id == BOS_ID || id == PAD_ID { continue; }
145 if id == EOS_ID { break; }
146
147 let token = match self.id_to_token.get(id as usize) {
148 Some(t) if !t.is_empty() => t.as_str(),
149 _ => continue,
150 };
151
152 if let Some(stripped) = token.strip_prefix('▁') {
154 if prev_needs_space && !out.is_empty() {
155 out.push(' ');
156 }
157 out.push_str(stripped);
158 prev_needs_space = true;
159 } else if let Some(stripped) = token.strip_prefix('Ġ') {
160 if !out.is_empty() { out.push(' '); }
162 out.push_str(stripped);
163 prev_needs_space = false;
164 } else {
165 out.push_str(token);
166 prev_needs_space = false;
167 }
168 }
169 out.trim().to_string()
170 }
171
172 fn vocab_size(&self) -> usize { self.id_to_token.len() }
173}
174
175#[derive(Debug, Clone, Copy)]
179enum InferenceMode {
180 SinglePass,
183 Autoregressive,
185}
186
187pub struct FormulaRecognizer {
191 session: Session,
192 tokenizer: Tokenizer,
193 mode: InferenceMode,
194 img_input: String,
196 pub decoder_enabled: bool,
201}
202
203impl FormulaRecognizer {
204 pub fn from_paths(
210 model_path: impl AsRef<Path>,
211 tokenizer_path: Option<&Path>,
212 ) -> Result<Self, OcrError> {
213 let session = Session::builder()?
214 .with_optimization_level(GraphOptimizationLevel::Level3)?
215 .commit_from_file(model_path)?;
216
217 let tokenizer = match tokenizer_path {
218 Some(p) => Tokenizer::from_json(p)?,
219 None => Tokenizer::fallback(),
220 };
221
222 let mode = detect_inference_mode(&session);
223 let img_input = find_image_input(&session);
224
225 eprintln!(
226 "[FormulaRec] modalità: {mode:?}, input immagine: '{img_input}', vocab: {}",
227 tokenizer.vocab_size()
228 );
229
230 Ok(Self { session, tokenizer, mode, img_input, decoder_enabled: false })
231 }
232
233 pub fn recognize(&self, image: &image::RgbImage) -> Result<FormulaResult, OcrError> {
243 if !self.decoder_enabled {
244 return Ok(FormulaResult { latex: String::new(), score: 0.0 });
245 }
246
247 let blob = preprocess(image);
248
249 match self.mode {
250 InferenceMode::SinglePass => self.recognize_single_pass(blob),
251 InferenceMode::Autoregressive => self.recognize_autoregressive(blob),
252 }
253 }
254
255 fn recognize_single_pass(&self, blob: Array4<f32>) -> Result<FormulaResult, OcrError> {
258 let outputs = self.session.run(
259 inputs![self.img_input.clone() => Tensor::from_array(blob)?]?
260 )?;
261
262 let (_, first) = outputs.iter().next()
263 .ok_or_else(|| OcrError::ModelOutput("FormulaNet: nessun output".into()))?;
264 let (shape, data_f32) = crate::compat::tensor_extract_with_shape_f32(&first)?;
265
266 eprintln!("[FormulaNet] single-pass output shape: {shape:?}");
267
268 match shape.as_slice() {
269 [1, t_len, v_len] => {
271 let t = *t_len as usize;
272 let v = *v_len as usize;
273 let mut ids: Vec<i64> = Vec::with_capacity(t);
274 let mut scores: Vec<f32> = Vec::with_capacity(t);
275 for step in 0..t {
276 let base = step * v;
277 let (idx, score) = argmax_slice(&data_f32[base..base + v]);
278 let id = idx as i64;
279 if id == EOS_ID { break; }
280 if id == PAD_ID || id == BOS_ID { continue; }
281 ids.push(id);
282 scores.push(score);
283 }
284 let latex = self.tokenizer.decode(&ids);
285 let score = mean_score(&scores);
286 Ok(FormulaResult { latex, score })
287 }
288
289 [1, _t] => {
291 let ids: Vec<i64> = data_f32.iter().map(|&f| f.round() as i64).collect();
293 let latex = self.tokenizer.decode(&ids);
294 Ok(FormulaResult { latex, score: 0.0 })
295 }
296
297 other => Err(OcrError::ModelOutput(format!(
298 "FormulaNet single-pass: shape output non riconosciuta: {other:?}"
299 ))),
300 }
301 }
302
303 fn recognize_autoregressive(&self, blob: Array4<f32>) -> Result<FormulaResult, OcrError> {
306 let dec_input_name = self.session.inputs.iter()
308 .find(|i| {
309 let n = i.name.to_lowercase();
310 n.contains("decoder") || n.contains("input_ids") || n.contains("tgt")
311 })
312 .map(|i| i.name.clone())
313 .unwrap_or_else(|| "decoder_input_ids".to_string());
314
315 let mut ids: Vec<i64> = vec![BOS_ID];
316 let mut scores: Vec<f32> = Vec::new();
317
318 for _ in 0..MAX_NEW_TOKENS {
319 let dec_len = ids.len();
321 let dec_arr: ndarray::Array2<i64> = ndarray::Array2::from_shape_vec(
322 (1, dec_len),
323 ids.clone(),
324 ).map_err(|e| OcrError::ModelInput(format!("dec_arr shape: {e}")))?;
325
326 let outputs = self.session.run(inputs![
327 self.img_input.clone() => Tensor::from_array(blob.clone())?,
328 dec_input_name.clone() => Tensor::from_array(dec_arr)?,
329 ]?)?;
330
331 let (_, last_out) = outputs.iter().next()
332 .ok_or_else(|| OcrError::ModelOutput("FormulaNet auto: nessun output".into()))?;
333 let (shape, data) = crate::compat::tensor_extract_with_shape_f32(&last_out)?;
334
335 let next_id = match shape.as_slice() {
337 [1, _t, v] => {
338 let v = *v as usize;
339 let last_step_base = (dec_len - 1) * v;
340 let (idx, score) = argmax_slice(&data[last_step_base..last_step_base + v]);
341 scores.push(score);
342 idx as i64
343 }
344 [1, v] => {
345 let (idx, score) = argmax_slice(&data[..(*v as usize)]);
346 scores.push(score);
347 idx as i64
348 }
349 other => return Err(OcrError::ModelOutput(format!(
350 "FormulaNet auto step: shape {other:?}"
351 ))),
352 };
353
354 if next_id == EOS_ID || next_id == PAD_ID { break; }
355 ids.push(next_id);
356 }
357
358 let decode_ids: Vec<i64> = ids.into_iter().skip(1).collect();
360 let latex = self.tokenizer.decode(&decode_ids);
361 let score = mean_score(&scores);
362 Ok(FormulaResult { latex, score })
363 }
364}
365
366fn preprocess(image: &image::RgbImage) -> Array4<f32> {
369 let s = FORMULA_INPUT_SIZE;
370 let resized = image::imageops::resize(image, s, s, image::imageops::FilterType::Triangle);
371 let mean = [0.485f32, 0.456, 0.406];
372 let std = [0.229f32, 0.224, 0.225];
373 let mut blob: Array4<f32> = Array::zeros((1, 3, s as usize, s as usize));
374 for y in 0..s as usize {
375 for x in 0..s as usize {
376 let p = resized.get_pixel(x as u32, y as u32);
377 for c in 0..3usize {
378 blob[[0, c, y, x]] = (p[c] as f32 / 255.0 - mean[c]) / std[c];
379 }
380 }
381 }
382 blob
383}
384
385fn detect_inference_mode(session: &Session) -> InferenceMode {
388 let has_dec_input = session.inputs.iter().any(|i| {
389 let n = i.name.to_lowercase();
390 n.contains("decoder") || n.contains("input_ids") || n.contains("tgt")
391 });
392 if has_dec_input {
393 InferenceMode::Autoregressive
394 } else {
395 InferenceMode::SinglePass
396 }
397}
398
399fn find_image_input(session: &Session) -> String {
400 session.inputs.iter()
401 .find(|i| {
402 let n = i.name.to_lowercase();
403 n == "x" || n == "image" || n == "img" || n.contains("pixel")
404 })
405 .or_else(|| session.inputs.first())
406 .map(|i| i.name.clone())
407 .unwrap_or_else(|| "x".to_string())
408}
409
410fn argmax_slice(s: &[f32]) -> (usize, f32) {
411 s.iter().copied().enumerate()
412 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
413 .unwrap_or((0, 0.0))
414}
415
416fn mean_score(v: &[f32]) -> f32 {
417 if v.is_empty() { 0.0 } else { v.iter().sum::<f32>() / v.len() as f32 }
418}