Skip to content

Commit 2eeff63

Browse files
authored
Merge pull request #158 from artiz/claude/wasm-ml-stage1
wasm: in-browser OCR via ONNX Runtime Web interop (#157 stage 1)
2 parents 42aad70 + 583b706 commit 2eeff63

25 files changed

Lines changed: 3935 additions & 790 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ onnxruntime_profile_*.json
88

99
# wasm-bindgen output for the docling-wasm demo (built from source, see its README)
1010
crates/docling-wasm/www/pkg/
11+
# ML models the scan demo fetches same-origin (download_dependencies.sh, run in www/)
12+
crates/docling-wasm/www/models/
1113

1214
# Secrets — never commit (holds CARGO_REGISTRY_TOKEN)
1315
.env

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/docling-pdf/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ default = ["ml"]
1818
# (`convert_text_layer` — lopdf content-stream parsing + the docling-parse
1919
# line sanitizer, the same extraction `--no-ocr` uses), which compiles for
2020
# wasm32-unknown-unknown.
21+
# The ONNX-free OCR pre/post half (`ocr_prep`) alone — what the wasm build
22+
# (#79 phase 2) pulls: pure Rust + the `image` crate, no ort/pdfium.
23+
ocr-prep = ["dep:image"]
2124
ml = [
25+
"ocr-prep",
2226
"dep:pdfium-render",
2327
"dep:ort",
2428
"dep:image",

crates/docling-pdf/src/layout.rs

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,12 @@ pub struct Region {
4747
pub b: f32,
4848
}
4949

50-
#[cfg(feature = "ml")]
5150
/// Base confidence threshold (docling-ibm-models `base_threshold`): the raw
5251
/// RT-DETR floor before docling's `LayoutPostprocessor` applies its stricter
5352
/// per-label thresholds ([`label_threshold`]).
5453
const THRESHOLD: f32 = 0.3;
55-
#[cfg(feature = "ml")]
56-
const SIDE: u32 = 640;
54+
/// RT-DETR's fixed square input side.
55+
pub const SIDE: u32 = 640;
5756

5857
/// Per-label confidence threshold, ported from docling's
5958
/// `LayoutPostprocessor.CONFIDENCE_THRESHOLDS`. The raw predictor keeps every
@@ -222,46 +221,81 @@ impl LayoutModel {
222221
let logits =
223222
&logits[p * num_queries * num_classes..(p + 1) * num_queries * num_classes];
224223
let boxes = &boxes[p * num_queries * 4..(p + 1) * num_queries * 4];
225-
226-
// sigmoid over every (query, class); take the top `num_queries` scores.
227-
let mut scored: Vec<(f32, usize)> = (0..num_queries * num_classes)
228-
.map(|idx| (sigmoid(logits[idx]), idx))
229-
.collect();
230-
scored.sort_unstable_by(|a, b| b.0.total_cmp(&a.0));
231-
scored.truncate(num_queries);
232-
233-
let mut regions = Vec::new();
234-
for (score, idx) in scored {
235-
if score <= THRESHOLD {
236-
continue;
237-
}
238-
let label_id = idx % num_classes;
239-
let q = idx / num_classes;
240-
let cx = boxes[q * 4];
241-
let cy = boxes[q * 4 + 1];
242-
let w = boxes[q * 4 + 2];
243-
let h = boxes[q * 4 + 3];
244-
// center_to_corners, then scale normalized coords to page points.
245-
let l = (cx - w / 2.0) * page_w;
246-
let t = (cy - h / 2.0) * page_h;
247-
let r = (cx + w / 2.0) * page_w;
248-
let b = (cy + h / 2.0) * page_h;
249-
regions.push(Region {
250-
label: LABELS.get(label_id).copied().unwrap_or("text"),
251-
score,
252-
l,
253-
t,
254-
r,
255-
b,
256-
});
257-
}
258-
all.push(regions);
224+
all.push(decode_layout(
225+
logits,
226+
boxes,
227+
num_queries,
228+
num_classes,
229+
*page_w,
230+
*page_h,
231+
));
259232
}
260233
Ok(all)
261234
}
262235
}
263236

264-
#[cfg(feature = "ml")]
265237
fn sigmoid(x: f32) -> f32 {
266238
1.0 / (1.0 + (-x).exp())
267239
}
240+
241+
/// Pack one page image into the model's `(1, 3, SIDE, SIDE)` input: resize
242+
/// (aspect ignored, RT-DETR convention), rescale to `[0,1]`, CHW. Shared
243+
/// with the browser build (#157), which delegates only the session call.
244+
#[cfg(feature = "ocr-prep")]
245+
pub fn layout_input(img: &image::RgbImage) -> Vec<f32> {
246+
let n = (SIDE * SIDE) as usize;
247+
let mut data = vec![0f32; 3 * n];
248+
let resized = image::imageops::resize(img, SIDE, SIDE, image::imageops::FilterType::Triangle);
249+
for (i, px) in resized.pixels().enumerate() {
250+
data[i] = px[0] as f32 / 255.0;
251+
data[n + i] = px[1] as f32 / 255.0;
252+
data[2 * n + i] = px[2] as f32 / 255.0;
253+
}
254+
data
255+
}
256+
257+
/// Decode one page's raw RT-DETR outputs into scored [`Region`]s in page
258+
/// points — sigmoid over every (query, class), top-`num_queries` kept, boxes
259+
/// converted center→corners and scaled. Shared with the browser build; the
260+
/// native batch path calls it per page, so both decode identically.
261+
pub fn decode_layout(
262+
logits: &[f32],
263+
boxes: &[f32],
264+
num_queries: usize,
265+
num_classes: usize,
266+
page_w: f32,
267+
page_h: f32,
268+
) -> Vec<Region> {
269+
let mut scored: Vec<(f32, usize)> = (0..num_queries * num_classes)
270+
.map(|idx| (sigmoid(logits[idx]), idx))
271+
.collect();
272+
scored.sort_unstable_by(|a, b| b.0.total_cmp(&a.0));
273+
scored.truncate(num_queries);
274+
275+
let mut regions = Vec::new();
276+
for (score, idx) in scored {
277+
if score <= THRESHOLD {
278+
continue;
279+
}
280+
let label_id = idx % num_classes;
281+
let q = idx / num_classes;
282+
let cx = boxes[q * 4];
283+
let cy = boxes[q * 4 + 1];
284+
let w = boxes[q * 4 + 2];
285+
let h = boxes[q * 4 + 3];
286+
// center_to_corners, then scale normalized coords to page points.
287+
let l = (cx - w / 2.0) * page_w;
288+
let t = (cy - h / 2.0) * page_h;
289+
let r = (cx + w / 2.0) * page_w;
290+
let b = (cy + h / 2.0) * page_h;
291+
regions.push(Region {
292+
label: LABELS.get(label_id).copied().unwrap_or("text"),
293+
score,
294+
l,
295+
t,
296+
r,
297+
b,
298+
});
299+
}
300+
regions
301+
}

crates/docling-pdf/src/lib.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,25 @@ pub mod layout;
2727
mod mets;
2828
#[cfg(feature = "ml")]
2929
mod ocr;
30+
#[cfg(feature = "ocr-prep")]
31+
pub mod ocr_prep;
3032
pub mod pdfium_backend;
3133
mod reading_order;
32-
#[cfg(feature = "ml")]
34+
// Pure-Rust region resampling (page→1024px box-average, crop→448 bilinear) —
35+
// available to the browser TableFormer path (#157 stage 3), not just `ml`.
36+
#[cfg(feature = "ocr-prep")]
3337
pub mod resample;
38+
#[cfg(feature = "ocr-prep")]
39+
pub mod scanned;
3440
#[cfg(feature = "ml")]
3541
pub mod tableformer;
3642
pub mod textparse;
37-
#[cfg(feature = "ml")]
38-
mod tf_match;
43+
#[cfg(feature = "ocr-prep")]
44+
pub mod tf_core;
45+
// docling's TableFormer cell matcher — pure Rust, shared with the browser
46+
// TableFormer path (#157 stage 3).
47+
#[cfg(feature = "ocr-prep")]
48+
pub mod tf_match;
3949
pub mod timing;
4050

4151
#[cfg(feature = "ml")]

0 commit comments

Comments
 (0)