Skip to content

Commit 3d41bb3

Browse files
committed
feat: add support for GLM-OCR model engine
- replace Quality enum with FormulaModel and TableModel options - implement FormulaEngine and TableEngine for model dispatch - add support for GLM-OCR in formula and table recognition - update CLI and model loading to handle new engine selections
1 parent 75099e4 commit 3d41bb3

6 files changed

Lines changed: 222 additions & 83 deletions

File tree

crates/papers-extract/src/bin/bench_formulas.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,18 @@ fn main() {
102102
let cache_dir = cli
103103
.model_cache_dir
104104
.unwrap_or_else(models::default_cache_dir);
105-
let model_paths =
106-
models::ensure_models(papers_extract::Quality::Fast, &cache_dir).expect("Model download");
105+
let model_paths = models::ensure_models(
106+
papers_extract::FormulaModel::PpFormulanet,
107+
papers_extract::TableModel::SlanetPlus,
108+
&cache_dir,
109+
)
110+
.expect("Model download");
107111

108112
eprintln!("Loading formula predictor...");
109113
let predictor = FormulaPredictor::new(
110-
&model_paths.formula_encoder,
111-
&model_paths.formula_decoder,
112-
&model_paths.formula_tokenizer,
114+
model_paths.formula_encoder.as_deref().expect("formula_encoder path"),
115+
model_paths.formula_decoder.as_deref().expect("formula_decoder path"),
116+
model_paths.formula_tokenizer.as_deref().expect("formula_tokenizer path"),
113117
)
114118
.expect("FormulaPredictor::new");
115119
eprintln!("Formula predictor ready\n");

crates/papers-extract/src/bin/dump.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,12 @@ fn main() {
9393

9494
// Load layout model
9595
eprintln!("Loading layout model...");
96-
let paths = models::ensure_models(papers_extract::Quality::Fast, &cache_dir)
97-
.expect("layout model files");
96+
let paths = models::ensure_models(
97+
papers_extract::FormulaModel::PpFormulanet,
98+
papers_extract::TableModel::SlanetPlus,
99+
&cache_dir,
100+
)
101+
.expect("layout model files");
98102
let layout = models::build_layout_detector(&paths.layout).expect("layout detector");
99103

100104
// Load PDF

crates/papers-extract/src/lib.rs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ pub struct ExtractOptions {
2525
pub confidence_threshold: f32,
2626
/// Whether to extract figures as images (default true).
2727
pub extract_images: bool,
28-
/// Quality mode — affects model selection for tables (default Fast).
29-
pub quality: Quality,
28+
/// Formula recognition model (default PpFormulanet).
29+
pub formula: FormulaModel,
30+
/// Table recognition model (default SlanetPlus).
31+
pub table: TableModel,
3032
/// Path to the pdfium binary (auto-detected if None).
3133
pub pdfium_path: Option<PathBuf>,
3234
/// Directory for ONNX model cache (auto-detected if None).
@@ -45,7 +47,8 @@ impl Default for ExtractOptions {
4547
dpi: 144,
4648
confidence_threshold: 0.3,
4749
extract_images: true,
48-
quality: Quality::default(),
50+
formula: FormulaModel::default(),
51+
table: TableModel::default(),
4952
pdfium_path: None,
5053
model_cache_dir: None,
5154
page: None,
@@ -55,15 +58,26 @@ impl Default for ExtractOptions {
5558
}
5659
}
5760

58-
/// Quality mode controls model selection for tables.
61+
/// Formula recognition model selection.
5962
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
60-
pub enum Quality {
61-
/// Fast mode (default): SLANet-Plus (7 MB).
63+
pub enum FormulaModel {
64+
/// pp-formulanet split encoder/decoder (default).
6265
#[default]
63-
Fast,
64-
/// Quality mode: PP-LCNet table classifier (6.5 MB) + SLANeXt-wired (351 MB).
65-
/// Better accuracy for complex tables.
66-
Quality,
66+
PpFormulanet,
67+
/// GLM-OCR vision-language model.
68+
GlmOcr,
69+
}
70+
71+
/// Table recognition model selection.
72+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
73+
pub enum TableModel {
74+
/// SLANet-Plus (7 MB, default).
75+
#[default]
76+
SlanetPlus,
77+
/// PP-LCNet classifier + SLANeXt-wired (~358 MB).
78+
SlanextWired,
79+
/// GLM-OCR vision-language model with table prompt.
80+
GlmOcr,
6781
}
6882

6983
/// Controls what debug output to produce.

crates/papers-extract/src/main.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::path::PathBuf;
22
use std::process;
33

44
use clap::{Parser, ValueEnum};
5-
use papers_extract::{DebugMode, ExtractOptions, Quality};
5+
use papers_extract::{DebugMode, ExtractOptions, FormulaModel, TableModel};
66

77
#[derive(Parser)]
88
#[command(
@@ -18,9 +18,13 @@ struct Cli {
1818
#[arg(short, long)]
1919
output: Option<PathBuf>,
2020

21-
/// Quality mode for table recognition
22-
#[arg(long, short, default_value = "fast")]
23-
quality: QualityArg,
21+
/// Formula recognition model
22+
#[arg(long, default_value = "pp-formulanet")]
23+
formula: FormulaArg,
24+
25+
/// Table recognition model
26+
#[arg(long, default_value = "slanet-plus")]
27+
table: TableArg,
2428

2529
/// DPI for page rendering
2630
#[arg(long, default_value = "144")]
@@ -56,11 +60,21 @@ struct Cli {
5660
}
5761

5862
#[derive(ValueEnum, Clone, Debug)]
59-
enum QualityArg {
63+
enum FormulaArg {
64+
/// pp-formulanet split encoder/decoder
65+
PpFormulanet,
66+
/// GLM-OCR vision-language model
67+
GlmOcr,
68+
}
69+
70+
#[derive(ValueEnum, Clone, Debug)]
71+
enum TableArg {
6072
/// SLANet-Plus (7 MB)
61-
Fast,
73+
SlanetPlus,
6274
/// PP-LCNet classifier + SLANeXt-wired (~358 MB)
63-
Quality,
75+
SlanextWired,
76+
/// GLM-OCR vision-language model
77+
GlmOcr,
6478
}
6579

6680
#[derive(ValueEnum, Clone, Debug)]
@@ -85,9 +99,14 @@ fn main() {
8599
dpi: cli.dpi,
86100
confidence_threshold: cli.confidence,
87101
extract_images: !cli.no_images,
88-
quality: match cli.quality {
89-
QualityArg::Fast => Quality::Fast,
90-
QualityArg::Quality => Quality::Quality,
102+
formula: match cli.formula {
103+
FormulaArg::PpFormulanet => FormulaModel::PpFormulanet,
104+
FormulaArg::GlmOcr => FormulaModel::GlmOcr,
105+
},
106+
table: match cli.table {
107+
TableArg::SlanetPlus => TableModel::SlanetPlus,
108+
TableArg::SlanextWired => TableModel::SlanextWired,
109+
TableArg::GlmOcr => TableModel::GlmOcr,
91110
},
92111
pdfium_path: cli.pdfium_path,
93112
model_cache_dir: cli.model_cache_dir,

crates/papers-extract/src/models.rs

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use oar_ocr::predictors::TableStructureRecognitionPredictor;
66
use crate::error::ExtractError;
77
use crate::formula::FormulaPredictor;
88
use crate::glm_ocr::{GlmOcrConfig, GlmOcrPredictor};
9-
use crate::Quality;
9+
use crate::{FormulaModel, TableModel};
1010

1111
/// Model file metadata for download.
1212
struct ModelFile {
@@ -90,14 +90,14 @@ const SLANEXT_WIRED: ModelFile = ModelFile {
9090
/// Resolved paths to all required model files.
9191
pub struct ModelPaths {
9292
pub layout: PathBuf,
93-
pub slanet_plus: PathBuf,
94-
pub table_dict: PathBuf,
95-
pub formula_encoder: PathBuf,
96-
pub formula_decoder: PathBuf,
97-
pub formula_tokenizer: PathBuf,
98-
// Quality-mode extras
93+
pub slanet_plus: Option<PathBuf>,
94+
pub table_dict: Option<PathBuf>,
95+
pub formula_encoder: Option<PathBuf>,
96+
pub formula_decoder: Option<PathBuf>,
97+
pub formula_tokenizer: Option<PathBuf>,
9998
pub table_classifier: Option<PathBuf>,
10099
pub slanext_wired: Option<PathBuf>,
100+
pub glm_ocr: Option<GlmOcrModelPaths>,
101101
}
102102

103103
/// Resolved paths for GLM-OCR models (separate from pipeline models).
@@ -120,31 +120,50 @@ pub fn default_cache_dir() -> PathBuf {
120120
.join("models")
121121
}
122122

123-
/// Ensure all models for the given quality mode are downloaded and return their paths.
123+
/// Ensure all models for the selected formula/table engines are downloaded.
124124
pub fn ensure_models(
125-
quality: Quality,
125+
formula: FormulaModel,
126+
table: TableModel,
126127
cache_dir: &Path,
127128
) -> Result<ModelPaths, ExtractError> {
128129
std::fs::create_dir_all(cache_dir)?;
129130

130-
// Always required
131+
// Layout detection is always required
131132
let layout = ensure_model(cache_dir, &LAYOUT_MODEL)?;
132-
let slanet_plus = ensure_model(cache_dir, &SLANET_PLUS)?;
133-
let table_dict = ensure_model(cache_dir, &TABLE_DICT)?;
134-
let formula_tokenizer = ensure_model(cache_dir, &FORMULA_TOKENIZER)?;
135-
136-
// Formula encoder/decoder (no auto-download, just check existence)
137-
let formula_encoder = ensure_local_model(cache_dir, &FORMULA_ENCODER)?;
138-
let formula_decoder = ensure_local_model(cache_dir, &FORMULA_DECODER)?;
139-
140-
// Table models selected by --quality
141-
let (table_classifier, slanext_wired) = match quality {
142-
Quality::Fast => (None, None),
143-
Quality::Quality => {
133+
134+
// Formula models (only for pp-formulanet)
135+
let (formula_encoder, formula_decoder, formula_tokenizer) = match formula {
136+
FormulaModel::PpFormulanet => {
137+
let enc = ensure_local_model(cache_dir, &FORMULA_ENCODER)?;
138+
let dec = ensure_local_model(cache_dir, &FORMULA_DECODER)?;
139+
let tok = ensure_model(cache_dir, &FORMULA_TOKENIZER)?;
140+
(Some(enc), Some(dec), Some(tok))
141+
}
142+
FormulaModel::GlmOcr => (None, None, None),
143+
};
144+
145+
// Table models (only for slanet variants)
146+
let (slanet_plus, table_dict, table_classifier, slanext_wired) = match table {
147+
TableModel::SlanetPlus => {
148+
let slanet = ensure_model(cache_dir, &SLANET_PLUS)?;
149+
let dict = ensure_model(cache_dir, &TABLE_DICT)?;
150+
(Some(slanet), Some(dict), None, None)
151+
}
152+
TableModel::SlanextWired => {
153+
let slanet = ensure_model(cache_dir, &SLANET_PLUS)?;
154+
let dict = ensure_model(cache_dir, &TABLE_DICT)?;
144155
let classifier = ensure_model(cache_dir, &TABLE_CLASSIFIER)?;
145156
let wired = ensure_model(cache_dir, &SLANEXT_WIRED)?;
146-
(Some(classifier), Some(wired))
157+
(Some(slanet), Some(dict), Some(classifier), Some(wired))
147158
}
159+
TableModel::GlmOcr => (None, None, None, None),
160+
};
161+
162+
// GLM-OCR models (needed if either formula or table uses glm-ocr)
163+
let glm_ocr = if formula == FormulaModel::GlmOcr || table == TableModel::GlmOcr {
164+
Some(ensure_glm_ocr_models(cache_dir)?)
165+
} else {
166+
None
148167
};
149168

150169
Ok(ModelPaths {
@@ -156,6 +175,7 @@ pub fn ensure_models(
156175
formula_tokenizer,
157176
table_classifier,
158177
slanext_wired,
178+
glm_ocr,
159179
})
160180
}
161181

@@ -393,21 +413,27 @@ fn platform_execution_providers() -> Vec<OrtExecutionProvider> {
393413
pub fn build_formula_predictor(
394414
paths: &ModelPaths,
395415
) -> Result<FormulaPredictor, ExtractError> {
396-
FormulaPredictor::new(
397-
&paths.formula_encoder,
398-
&paths.formula_decoder,
399-
&paths.formula_tokenizer,
400-
)
416+
let enc = paths.formula_encoder.as_ref()
417+
.ok_or_else(|| ExtractError::Model("formula_encoder path missing".into()))?;
418+
let dec = paths.formula_decoder.as_ref()
419+
.ok_or_else(|| ExtractError::Model("formula_decoder path missing".into()))?;
420+
let tok = paths.formula_tokenizer.as_ref()
421+
.ok_or_else(|| ExtractError::Model("formula_tokenizer path missing".into()))?;
422+
FormulaPredictor::new(enc, dec, tok)
401423
}
402424

403425
/// Build a standalone table structure recognition predictor.
404426
pub fn build_table_predictor(
405427
paths: &ModelPaths,
406428
) -> Result<TableStructureRecognitionPredictor, ExtractError> {
429+
let dict = paths.table_dict.as_ref()
430+
.ok_or_else(|| ExtractError::Model("table_dict path missing".into()))?;
431+
let slanet = paths.slanet_plus.as_ref()
432+
.ok_or_else(|| ExtractError::Model("slanet_plus path missing".into()))?;
407433
let config = ort_config();
408434
TableStructureRecognitionPredictor::builder()
409-
.dict_path(&paths.table_dict)
435+
.dict_path(dict)
410436
.with_ort_config(config)
411-
.build(&paths.slanet_plus)
437+
.build(slanet)
412438
.map_err(|e| ExtractError::Model(format!("Failed to build table predictor: {e}")))
413439
}

0 commit comments

Comments
 (0)