Skip to content

feat(xberg-wasm): OCR results include each line's coordinates, and JS apps can plug in their own OCR and NER#1259

Merged
Goldziher merged 12 commits into
mainfrom
feat/wasm-ocr-line-geometry
Jul 19, 2026
Merged

feat(xberg-wasm): OCR results include each line's coordinates, and JS apps can plug in their own OCR and NER#1259
Goldziher merged 12 commits into
mainfrom
feat/wasm-ocr-line-geometry

Conversation

@tobocop2

@tobocop2 tobocop2 commented Jul 18, 2026

Copy link
Copy Markdown
Member

Closes #1266. Design and implementation by @jamon8888 in #1253; his commits carry his authorship, review fixes are separate commits.

What was broken

When a browser app ran OCR through the wasm package, it got back one undifferentiated blob of text. There was no way to know where each recognized line sits in the image, so the app could not highlight a line, scroll to it, or overlay it on the page. There was also no way for the app to plug in its own OCR or entity-detection engine: the wasm build either bundled one or you had nothing.

What this adds

XbergEngine, constructed with a config and an injection object, exposing extract(), ocr(), and ner(). An injected OCR backend returns { text, lines: [{ text, confidence, bbox? }] }; a backend without geometry degrades to an empty lines array. An injected NER backend works the same way, with a public JsNerBridge adapter for core consumers. All bridge calls run under a configurable timeout with leak-safe cleanup.

NerBackend's async_trait futures are ?Send on wasm32 only (JS futures are not Send; the target is single-threaded). Native contracts are unchanged.

The hand-written bridge/engine modules are declared through custom_rust_modules in alef.toml; the regenerated lib.rs is included, no hand edits.

Verification

cargo check/clippy --all-targets clean on wasm32; host build clean under workspace feature unification. engine_ocr.test.ts and engine_ner.test.ts cover geometry round-trip, degrade paths, category forwarding, and error paths against the built package. Host-target cargo check -p xberg-wasm in isolation fails on main already (tokio-gated paths); unchanged here.

BLOCKED follow-up (#1269): in-crate #[wasm_bindgen_test] suites wait on xberg-io/alef#182 shipping in a release.

@tobocop2

tobocop2 commented Jul 18, 2026

Copy link
Copy Markdown
Member Author

@jamon8888 tagging you here since this is your work. One scoped feature (the OCR bridge with per-line geometry and the engine handle). This is the shape we look for when reviewing code for next time.

@tobocop2

tobocop2 commented Jul 18, 2026

Copy link
Copy Markdown
Member Author

Follow-up: once xberg-io/alef#182 lands and the pin is bumped, the in-crate wasm-bindgen test suites can come back via [crates.wasm.extra_dev_dependencies]. The e2e vitest coverage in this PR carries the same cases until then.

@tobocop2 tobocop2 changed the title feat(xberg-wasm): injectable OCR bridge with per-line geometry feat(xberg-wasm): injectable OCR and NER bridges with per-line OCR geometry Jul 18, 2026
@tobocop2 tobocop2 changed the title feat(xberg-wasm): injectable OCR and NER bridges with per-line OCR geometry feat(xberg-wasm): report where each OCR line sits on the page, and let the browser supply its own OCR and NER engines Jul 18, 2026
@tobocop2 tobocop2 changed the title feat(xberg-wasm): report where each OCR line sits on the page, and let the browser supply its own OCR and NER engines feat(xberg-wasm): OCR results include each line's coordinates, and JS apps can plug in their own OCR and NER Jul 18, 2026
@Goldziher

Copy link
Copy Markdown
Member

Wow, nice

@tobocop2
tobocop2 force-pushed the feat/wasm-ocr-line-geometry branch from 5e459c6 to 86c3317 Compare July 18, 2026 05:45
jamon8888 and others added 12 commits July 19, 2026 14:50
…gine handle

Adds a hand-written JS bridge layer to the wasm crate: an injectable OCR
backend whose results carry per-line text, confidence, and bounding boxes
instead of a flat string, a promise-timeout helper for all bridge calls,
and an XbergEngine handle that exposes extract() and ocr() to JS hosts.

Carried over from PR #1253, trimmed to the OCR surface that compiles
against current main.

Co-authored-by: Candy Nguyen <cdy.nguyen@gmail.com>
… OCR errors

Review fixes from PR #1253: the timeout racer's cleanup chain now hangs
off the raced promise with both handlers attached, so a rejecting bridge
call can no longer surface as an unhandled rejection (fatal by default in
Node); a failure to arm the race logs a console warning instead of
silently dropping the timeout; the dead Tesseract fallback arms are gone
along with the module doc claiming a fallback exists; and the no-backend
error tells the caller what to inject instead of leaking crate
internals.
Ports the OCR bridge test cases from PR #1253 to the wasm e2e vitest
harness so they run against the actually built package: per-line geometry
round-trip, the missing-lines degrade path, language forwarding, and the
no-backend / empty-input / malformed-injection error paths.
Registers src/bridge/ and src/engine.rs under custom_rust_modules in
alef.toml so regeneration emits their declarations into the generated
lib.rs instead of requiring hand edits. Includes the regenerated
crates/xberg-wasm/src/lib.rs. Other regen drift (crawlberg 1.0.3 pin in
alef.toml vs 1.0.5 in committed manifests, docs formatting) is
pre-existing and left untouched.
… split

Adds a NER bridge mirroring the OCR bridge: an injected ner(text,
categories) object resolves entities through the same timeout machinery,
XbergEngine gains a ner() method, and JsNerBridge adapts the injected
object to the NerBackend trait for core consumers.

NerBackend's async_trait futures become ?Send on wasm32: a JS bridge's
futures hold JsValues driven by wasm_bindgen_futures, which are not Send,
and the target is single-threaded. Every other target keeps the plain
Send contract unchanged.

Carried over from PR #1253, trimmed to the injected path.

Co-authored-by: Candy Nguyen <cdy.nguyen@gmail.com>
Custom entity categories survive the JS boundary in both directions:
category_wire_name sends the raw label instead of serde's object form
(which emptied to an unnamed category), and unknown category names from
JS parse through EntityCategory::from, becoming zero-shot Custom labels
instead of being silently dropped. JsNerBridge is public API and its
async_trait form mirrors the trait's per-target split so the module also
compiles for host targets. The js_from_any helper is shared from the
bridge root instead of duplicated per module, and the no-backend error
tells the caller what to inject.
Ports the NER bridge cases from PR #1253 to the wasm e2e vitest harness:
entities round-trip through an injected stub, category forwarding
(built-in and custom labels as plain strings), and the no-backend and
malformed-injection error paths.
In the workspace build, feature unification lets xberg-wasm compile for
host targets, where NerBackend keeps its plain Send async_trait form and
JsValue is not Send, so the adapter impl failed with 'future cannot be
sent between threads safely'. The impl (and its two imports) now exist
only on wasm32; a JS bridge has nothing to bridge off-wasm. Verified on
host with unified features and on wasm32 with clippy all-targets.
- delete the dead, unwired JsNerBridge / NerBackend impl and resolve_ner/resolve_ocr
  wrappers; engine.rs calls resolve_*_with_timeout directly.
- rename OcrBbox { w, h } -> { width, height } (readability; disambiguates from the
  other bbox shapes); update e2e TS.
- Promise::from -> Promise::resolve(&result) so a synchronous injected callback yields
  a clear error instead of 'TypeError: .then is not a function'.
- validate bridgeTimeoutMs: reject non-finite / negative instead of saturating to 0.
- document the get_timeout_racer new Function(...) CSP ('unsafe-eval') constraint.
@Goldziher
Goldziher force-pushed the feat/wasm-ocr-line-geometry branch from 373a580 to c71c60b Compare July 19, 2026 12:51
@Goldziher
Goldziher merged commit 5beda31 into main Jul 19, 2026
8 checks passed
@Goldziher
Goldziher deleted the feat/wasm-ocr-line-geometry branch July 19, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: let the browser supply its own entity-detection engine to the wasm engine

3 participants