Skip to main content

pdf_engine/
model.rs

1//! The model tiers' shared surface: specs, failures and install paths (#641).
2//!
3//! Always compiled, with or without the `ort-layout` cargo feature: the
4//! specs, the failure types and the path resolution of the shared ONNX
5//! Runtime layer are plain Rust, so a build without the tier still reports a
6//! missing model with the typed [`ModelError`] rather than a string. Only
7//! the session half of `pdf_ocr::runtime` -- `init`, `builder`, `session`,
8//! `SharedSession` -- needs the `ort` feature and the `ort` crate.
9//!
10//! [`LAYOUT_EP03`] is the balanced tier's model. [`balanced_unavailable`]
11//! is what [`Quality::Balanced`](crate::Quality) answers on a build without
12//! the tier.
13
14// Copyright (c) 2026 Innovation Trigger B.V.
15//
16// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
17// the PDFluent Commercial Licence. See the LICENSE file in this repository --
18// that file travels with the copy you received, which a URL does not.
19
20pub use pdf_ocr::runtime::{
21    default_model_root, model_error, resolve_model_file, resolve_model_root, resolve_ort_dylib,
22    ModelError, ModelErrorKind, ModelSpec, RuntimeConfig, ORT_VERSION,
23};
24
25#[cfg(not(feature = "ort-layout"))]
26use crate::EngineError;
27use crate::Result;
28
29/// The layout model: Heron-ft ep03 (RT-DETRv2), ONNX export.
30///
31/// One definition for both builds: the session loader reads it behind
32/// `ort-layout`, and the balanced error below names it without the feature.
33pub const LAYOUT_EP03: ModelSpec = ModelSpec::new(
34    "layout-heron-ep03",
35    "heron_ep03.onnx",
36    "b7cc5d31f3223c74cc477a1386b894793618aeb398ca59cf5eb7264ec61d8afa",
37    "pdfluent models install layout",
38);
39
40/// The tables model: TATR v1.1-all (Table Transformer structure), ONNX export.
41///
42/// One definition for the installer on every build: `pdfluent models install
43/// tables` resolves this spec to the install path, and the TATR stage resolves
44/// it the same way through its own [`TatrOptions`](crate::TatrOptions) default
45/// -- so an install is found without flags. The stage itself reads
46/// `learned::tables_tatr::TATR_SPEC` behind `ort-tables`; the name, file and
47/// digest here MUST match it, pinned by
48/// `the_installer_tables_spec_matches_the_stage_spec` in
49/// `tests/tatr_engine_hook.rs`, which runs under the `ort-tables` CI job.
50pub const TABLES_TATR: ModelSpec = ModelSpec::new(
51    "tatr-v1.1-all",
52    "tatr-v1.1-all.onnx",
53    "8c28418690b896a845a53633736a62c22ef8c9a515a23c06e9976e61360f1fcf",
54    "pdfluent models install tables",
55);
56
57/// The [`Quality::Balanced`](crate::Quality) failure on a build without the
58/// tier: `MODEL_NOT_AVAILABLE` naming the cargo feature to rebuild with and
59/// the install command, carried as [`EngineError::Io`] -- no new variants.
60#[cfg(not(feature = "ort-layout"))]
61pub(crate) fn balanced_unavailable() -> EngineError {
62    EngineError::Io(std::io::Error::from(ModelError::feature_not_compiled(
63        &LAYOUT_EP03,
64        "ort-layout",
65    )))
66}
67
68/// Run the balanced tier over the document, or fail loud without the feature.
69///
70/// With `ort-layout` this is `balanced_markdown` with the model dir and the
71/// TATR opt-in the caller gave; without it, `MODEL_NOT_AVAILABLE` naming
72/// the feature and the install command. `PdfDocument` calls this before
73/// validating, because the balanced tier is markdown-only and the block
74/// validation would refuse it first.
75pub(crate) fn balanced_markdown_for(
76    doc: &crate::PdfDocument,
77    options: &crate::ExtractOptions,
78) -> Result<String> {
79    #[cfg(feature = "ort-layout")]
80    {
81        let mut config = RuntimeConfig::new();
82        if let Some(dir) = &options.model_dir {
83            config = config.with_model_dir(dir.clone());
84        }
85        let mut balanced =
86            crate::balanced::BalancedOptions::default().keep_furniture(options.keep_furniture);
87        // The TATR opt-in rides into the balanced tier: `Quality::Balanced`
88        // plus `with_tatr` structures the kept table regions with TATR
89        // first (track B, P4). Unset, the tier runs as it always has.
90        if let Some(tatr) = options.tatr() {
91            balanced = balanced.with_tatr(tatr.clone());
92        }
93        crate::balanced::balanced_markdown(doc, &config, &balanced)
94    }
95    #[cfg(not(feature = "ort-layout"))]
96    {
97        let _ = (doc, options);
98        Err(balanced_unavailable())
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn the_layout_spec_names_the_ep03_package() {
108        assert_eq!(LAYOUT_EP03.name, "layout-heron-ep03");
109        assert_eq!(LAYOUT_EP03.file, "heron_ep03.onnx");
110        assert_eq!(
111            LAYOUT_EP03.sha256,
112            "b7cc5d31f3223c74cc477a1386b894793618aeb398ca59cf5eb7264ec61d8afa"
113        );
114        assert_eq!(
115            LAYOUT_EP03.install_command,
116            "pdfluent models install layout"
117        );
118        assert_eq!(ORT_VERSION, "1.23.2");
119    }
120
121    #[test]
122    fn the_tables_spec_names_the_tatr_package() {
123        assert_eq!(TABLES_TATR.name, "tatr-v1.1-all");
124        assert_eq!(TABLES_TATR.file, "tatr-v1.1-all.onnx");
125        assert_eq!(
126            TABLES_TATR.sha256,
127            "8c28418690b896a845a53633736a62c22ef8c9a515a23c06e9976e61360f1fcf"
128        );
129        assert_eq!(
130            TABLES_TATR.install_command,
131            "pdfluent models install tables"
132        );
133    }
134
135    #[cfg(not(feature = "ort-layout"))]
136    #[test]
137    fn balanced_unavailable_names_the_feature_and_the_install() {
138        let error = balanced_unavailable();
139        let EngineError::Io(io) = &error else {
140            panic!("a model failure rides as Io");
141        };
142        let model = model_error(io).expect("the chain carries the ModelError");
143        assert_eq!(model.kind, ModelErrorKind::ModelNotAvailable);
144        assert_eq!(model.kind.code(), "MODEL_NOT_AVAILABLE");
145        let message = error.to_string();
146        assert!(
147            message.contains("`ort-layout`"),
148            "the message names the cargo feature: {message}"
149        );
150        assert!(
151            message.contains("pdfluent models install layout"),
152            "the message names the install command: {message}"
153        );
154    }
155}