Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5a837fc
wasm: in-browser OCR via ONNX Runtime Web interop (#157 stage 1)
artiz Jul 24, 2026
76add75
wasm: lite-profile scanned pipeline in the browser (#157 stage 2)
artiz Jul 24, 2026
ebd2cb6
wasm: fix workspace clippy — PdfPage::from_cells absorbs the ml image…
artiz Jul 24, 2026
16a0955
wasm: polarity normalization for dark-mode captures (#157)
artiz Jul 24, 2026
47c1783
wasm: recognition-language selector in the browser demos
artiz Jul 24, 2026
c45388a
wasm demos: working cyrillic recognition model URL
artiz Jul 24, 2026
3411366
images: silence dead-code warnings in the no-fetch build shape
artiz Jul 24, 2026
644520d
wasm demos: cyrillic recognition via PP-OCRv5
artiz Jul 24, 2026
a154e93
wasm demos: errors-only ort logging
artiz Jul 24, 2026
ce64ed5
wasm demos: busy-state UI + download progress + quiet ort logs
artiz Jul 24, 2026
df2920f
wasm scan demo: fetch the layout model that models-v1 actually hosts
artiz Jul 24, 2026
a3ccf08
wasm scan demo: load the layout model same-origin (release assets are…
artiz Jul 24, 2026
e75a135
wasm scan demo: fetch layout into ./models/ via download_dependencies…
artiz Jul 24, 2026
bf92b9d
wasm scan demo: per-page timing
artiz Jul 24, 2026
4e2123e
wasm scan demo: surface int8 vs fp32 layout in the ready line
artiz Jul 24, 2026
6eaed97
wasm demos: multi-threaded ORT via a cross-origin-isolation service w…
artiz Jul 24, 2026
87f4be6
wasm scan demo: pipeline pdf.js rasterization with conversion
artiz Jul 24, 2026
7b2e35c
wasm scan demo: run the pipeline in a Web Worker (responsive UI)
artiz Jul 24, 2026
8f2be44
wasm scan demo: rasterize on the main thread, OCR in the worker
artiz Jul 24, 2026
df455c6
#157 stage 2: padded rec batching in the browser (fewer ORT calls)
artiz Jul 24, 2026
1ab32b8
Revert "#157 stage 2: padded rec batching in the browser (fewer ORT c…
artiz Jul 24, 2026
c48b7d3
docling-wasm: add LICENSE file for wasm-pack
artiz Jul 24, 2026
3002bb1
#157 stage 3 (phase 1): extract ort-free TableFormer core into tf_core
artiz Jul 24, 2026
837cdcf
wasm scan demo: give pdf.js its wasm/cmap/font asset URLs
artiz Jul 24, 2026
a47738c
#157 stage 3 (phase 2): expose resample + tf_match under ocr-prep
artiz Jul 24, 2026
c78e638
#157 stage 3 (phase 3a): move the ort-free table-rows tail into tf_core
artiz Jul 24, 2026
5cf2784
#157 stage 3 (phase 3b/3c): browser TableFormer via ort-web interop
artiz Jul 24, 2026
98f06b4
#157 stage 3 (phase 4): TableFormer demo wiring
artiz Jul 24, 2026
266b158
wasm scan: keep page_count/finish exported after the add_page split
artiz Jul 24, 2026
0c65226
tf_core: cargo fmt (module order + line wrapping)
artiz Jul 24, 2026
d36b751
wasm scan demo: Hugging Face fallback for the layout model
artiz Jul 25, 2026
7c48df3
wasm scan: log the layout region-label histogram
artiz Jul 25, 2026
42235b8
wasm scan: render the region-label diagnostic on the page
artiz Jul 25, 2026
bc55411
#157 stage 3: OCR table interiors so browser tables fill
artiz Jul 25, 2026
fb9e799
wasm demo: Hugging Face fallback for the TableFormer graphs
artiz Jul 25, 2026
829e28d
wasm demo: TableFormer graphs load from the HF repo root
artiz Jul 25, 2026
05685e1
wasm demo: surface the real TableFormer load error
artiz Jul 25, 2026
e05c6fa
wasm demo: load models from device files (no download)
artiz Jul 25, 2026
9e2e3fb
wasm docs: document the full in-browser ML pipeline + phone run guide
artiz Jul 25, 2026
583b706
wasm docs: correct the int8-encoder perf note with measured numbers
artiz Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ onnxruntime_profile_*.json

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

# Secrets — never commit (holds CARGO_REGISTRY_TOKEN)
.env
Expand Down
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/docling-pdf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ default = ["ml"]
# (`convert_text_layer` — lopdf content-stream parsing + the docling-parse
# line sanitizer, the same extraction `--no-ocr` uses), which compiles for
# wasm32-unknown-unknown.
# The ONNX-free OCR pre/post half (`ocr_prep`) alone — what the wasm build
# (#79 phase 2) pulls: pure Rust + the `image` crate, no ort/pdfium.
ocr-prep = ["dep:image"]
ml = [
"ocr-prep",
"dep:pdfium-render",
"dep:ort",
"dep:image",
Expand Down
110 changes: 72 additions & 38 deletions crates/docling-pdf/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,12 @@ pub struct Region {
pub b: f32,
}

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

/// Per-label confidence threshold, ported from docling's
/// `LayoutPostprocessor.CONFIDENCE_THRESHOLDS`. The raw predictor keeps every
Expand Down Expand Up @@ -222,46 +221,81 @@ impl LayoutModel {
let logits =
&logits[p * num_queries * num_classes..(p + 1) * num_queries * num_classes];
let boxes = &boxes[p * num_queries * 4..(p + 1) * num_queries * 4];

// sigmoid over every (query, class); take the top `num_queries` scores.
let mut scored: Vec<(f32, usize)> = (0..num_queries * num_classes)
.map(|idx| (sigmoid(logits[idx]), idx))
.collect();
scored.sort_unstable_by(|a, b| b.0.total_cmp(&a.0));
scored.truncate(num_queries);

let mut regions = Vec::new();
for (score, idx) in scored {
if score <= THRESHOLD {
continue;
}
let label_id = idx % num_classes;
let q = idx / num_classes;
let cx = boxes[q * 4];
let cy = boxes[q * 4 + 1];
let w = boxes[q * 4 + 2];
let h = boxes[q * 4 + 3];
// center_to_corners, then scale normalized coords to page points.
let l = (cx - w / 2.0) * page_w;
let t = (cy - h / 2.0) * page_h;
let r = (cx + w / 2.0) * page_w;
let b = (cy + h / 2.0) * page_h;
regions.push(Region {
label: LABELS.get(label_id).copied().unwrap_or("text"),
score,
l,
t,
r,
b,
});
}
all.push(regions);
all.push(decode_layout(
logits,
boxes,
num_queries,
num_classes,
*page_w,
*page_h,
));
}
Ok(all)
}
}

#[cfg(feature = "ml")]
fn sigmoid(x: f32) -> f32 {
1.0 / (1.0 + (-x).exp())
}

/// Pack one page image into the model's `(1, 3, SIDE, SIDE)` input: resize
/// (aspect ignored, RT-DETR convention), rescale to `[0,1]`, CHW. Shared
/// with the browser build (#157), which delegates only the session call.
#[cfg(feature = "ocr-prep")]
pub fn layout_input(img: &image::RgbImage) -> Vec<f32> {
let n = (SIDE * SIDE) as usize;
let mut data = vec![0f32; 3 * n];
let resized = image::imageops::resize(img, SIDE, SIDE, image::imageops::FilterType::Triangle);
for (i, px) in resized.pixels().enumerate() {
data[i] = px[0] as f32 / 255.0;
data[n + i] = px[1] as f32 / 255.0;
data[2 * n + i] = px[2] as f32 / 255.0;
}
data
}

/// Decode one page's raw RT-DETR outputs into scored [`Region`]s in page
/// points — sigmoid over every (query, class), top-`num_queries` kept, boxes
/// converted center→corners and scaled. Shared with the browser build; the
/// native batch path calls it per page, so both decode identically.
pub fn decode_layout(
logits: &[f32],
boxes: &[f32],
num_queries: usize,
num_classes: usize,
page_w: f32,
page_h: f32,
) -> Vec<Region> {
let mut scored: Vec<(f32, usize)> = (0..num_queries * num_classes)
.map(|idx| (sigmoid(logits[idx]), idx))
.collect();
scored.sort_unstable_by(|a, b| b.0.total_cmp(&a.0));
scored.truncate(num_queries);

let mut regions = Vec::new();
for (score, idx) in scored {
if score <= THRESHOLD {
continue;
}
let label_id = idx % num_classes;
let q = idx / num_classes;
let cx = boxes[q * 4];
let cy = boxes[q * 4 + 1];
let w = boxes[q * 4 + 2];
let h = boxes[q * 4 + 3];
// center_to_corners, then scale normalized coords to page points.
let l = (cx - w / 2.0) * page_w;
let t = (cy - h / 2.0) * page_h;
let r = (cx + w / 2.0) * page_w;
let b = (cy + h / 2.0) * page_h;
regions.push(Region {
label: LABELS.get(label_id).copied().unwrap_or("text"),
score,
l,
t,
r,
b,
});
}
regions
}
16 changes: 13 additions & 3 deletions crates/docling-pdf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,25 @@ pub mod layout;
mod mets;
#[cfg(feature = "ml")]
mod ocr;
#[cfg(feature = "ocr-prep")]
pub mod ocr_prep;
pub mod pdfium_backend;
mod reading_order;
#[cfg(feature = "ml")]
// Pure-Rust region resampling (page→1024px box-average, crop→448 bilinear) —
// available to the browser TableFormer path (#157 stage 3), not just `ml`.
#[cfg(feature = "ocr-prep")]
pub mod resample;
#[cfg(feature = "ocr-prep")]
pub mod scanned;
#[cfg(feature = "ml")]
pub mod tableformer;
pub mod textparse;
#[cfg(feature = "ml")]
mod tf_match;
#[cfg(feature = "ocr-prep")]
pub mod tf_core;
// docling's TableFormer cell matcher — pure Rust, shared with the browser
// TableFormer path (#157 stage 3).
#[cfg(feature = "ocr-prep")]
pub mod tf_match;
pub mod timing;

#[cfg(feature = "ml")]
Expand Down
Loading
Loading