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}