Skip to content

Commit fa72393

Browse files
artizclaude
andcommitted
wasm: recognition-language selector in the browser demos
The demo pages hardcoded the English PP-OCRv3 model, so any non-Latin scan (e.g. Cyrillic) decoded to mojibake through the wrong dictionary. The wasm API is language-agnostic — model and dict are injected from JS — so the fix is demo-side only: both ocr.html and scan.html get a recognition-language <select> (en / cyrillic / ch) backed by a lazy per-language ort.InferenceSession cache; models load on first use and stay browser-cached (~10 MB each). Refs #157 Signed-off-by: artiz <artem.kustikov@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EY5KAiquN4YpVf2PXEQkVT
1 parent 5ebe981 commit fa72393

2 files changed

Lines changed: 106 additions & 47 deletions

File tree

crates/docling-wasm/www/ocr.html

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,48 +34,75 @@ <h1>docling.rs → wasm: OCR a scanned image in your browser</h1>
3434
</p>
3535
<div id="drop">drop a scanned image here (PNG/JPEG/TIFF/WebP) or click to pick</div>
3636
<input type="file" id="pick" accept=".png,.jpg,.jpeg,.tif,.tiff,.webp,.bmp" hidden>
37-
<p id="meta">loading wasm + model …</p>
37+
<p>
38+
Recognition language:
39+
<select id="lang">
40+
<option value="en" selected>English (en_PP-OCRv3)</option>
41+
<option value="cyrillic">Cyrillic + Latin (cyrillic_PP-OCRv3)</option>
42+
<option value="ch">Chinese + Latin (ch_PP-OCRv3)</option>
43+
</select>
44+
<span id="meta"></span>
45+
</p>
46+
3847
<div id="out"></div>
3948

4049
<script type="module">
4150
import * as ort from "https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.mjs";
4251
import init, { ocr_image } from "./pkg/docling_wasm.js";
4352

44-
const MODEL_URL =
45-
"https://huggingface.co/SWHL/RapidOCR/resolve/main/PP-OCRv3/en_PP-OCRv3_rec_infer.onnx";
46-
const DICT_URL =
47-
"https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/main/ppocr/utils/en_dict.txt";
48-
4953
const meta = document.getElementById("meta");
5054
const out = document.getElementById("out");
55+
const lang = document.getElementById("lang");
5156

5257
await init();
53-
meta.textContent = "fetching recognition model (~10 MB, cached after the first load) …";
54-
const [model, dict] = await Promise.all([
55-
fetch(MODEL_URL, { cache: "force-cache" }).then((r) => r.arrayBuffer()),
56-
fetch(DICT_URL, { cache: "force-cache" }).then((r) => r.text()),
57-
]);
58-
const session = await ort.InferenceSession.create(model, {
59-
executionProviders: ["wasm"],
60-
});
61-
meta.textContent = "ready — drop a scanned image";
62-
63-
// The RecSession wrapper docling_wasm.ocr_image expects: feed the CHW
64-
// batch, return the (only) output tensor as { data, dims }.
65-
const rec = {
66-
run: async (n, h, w, data) => {
67-
const feeds = { [session.inputNames[0]]: new ort.Tensor("float32", data, [n, 3, h, w]) };
68-
const results = await session.run(feeds);
69-
const tensor = results[session.outputNames[0]];
70-
return { data: tensor.data, dims: Array.from(tensor.dims) };
58+
const REC_MODELS = {
59+
en: {
60+
model: "https://huggingface.co/SWHL/RapidOCR/resolve/main/PP-OCRv3/en_PP-OCRv3_rec_infer.onnx",
61+
dict: "https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/main/ppocr/utils/en_dict.txt",
62+
},
63+
cyrillic: {
64+
model: "https://huggingface.co/SWHL/RapidOCR/resolve/main/PP-OCRv3/cyrillic_PP-OCRv3_rec_infer.onnx",
65+
dict: "https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/main/ppocr/utils/dict/cyrillic_dict.txt",
66+
},
67+
ch: {
68+
model: "https://huggingface.co/SWHL/RapidOCR/resolve/main/PP-OCRv3/ch_PP-OCRv3_rec_infer.onnx",
69+
dict: "https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/main/ppocr/utils/ppocr_keys_v1.txt",
7170
},
7271
};
72+
const recCache = {};
73+
async function recFor(lang) {
74+
if (!recCache[lang]) {
75+
meta.textContent = `fetching ${lang} recognition model (~10 MB, cached) ...`;
76+
const spec = REC_MODELS[lang];
77+
const [model, dict] = await Promise.all([
78+
fetch(spec.model, { cache: "force-cache" }).then((r) => r.arrayBuffer()),
79+
fetch(spec.dict, { cache: "force-cache" }).then((r) => r.text()),
80+
]);
81+
const session = await ort.InferenceSession.create(model, { executionProviders: ["wasm"] });
82+
recCache[lang] = {
83+
dict,
84+
rec: {
85+
run: async (n, h, w, data) => {
86+
const results = await session.run({
87+
[session.inputNames[0]]: new ort.Tensor("float32", data, [n, 3, h, w]),
88+
});
89+
const t = results[session.outputNames[0]];
90+
return { data: t.data, dims: Array.from(t.dims) };
91+
},
92+
},
93+
};
94+
}
95+
return recCache[lang];
96+
}
97+
await recFor(lang.value);
98+
meta.textContent = "ready — drop a scanned image";
7399

74100
async function handle(file) {
75101
out.textContent = "";
76102
meta.textContent = `recognizing ${file.name} …`;
77103
const started = performance.now();
78104
try {
105+
const { dict, rec } = await recFor(lang.value);
79106
const bytes = new Uint8Array(await file.arrayBuffer());
80107
const md = await ocr_image(bytes, dict, rec, "md");
81108
out.textContent = md || "(no text found)";

crates/docling-wasm/www/scan.html

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,16 @@ <h1>docling.rs → wasm: scanned PDF / image, full lite profile</h1>
3434
</p>
3535
<div id="drop">drop a scanned PDF or image here, or click to pick</div>
3636
<input type="file" id="pick" accept=".pdf,.png,.jpg,.jpeg,.tif,.tiff,.webp,.bmp" hidden>
37-
<p id="meta">loading wasm …</p>
37+
<p>
38+
Recognition language:
39+
<select id="lang">
40+
<option value="en" selected>English (en_PP-OCRv3)</option>
41+
<option value="cyrillic">Cyrillic + Latin (cyrillic_PP-OCRv3)</option>
42+
<option value="ch">Chinese + Latin (ch_PP-OCRv3)</option>
43+
</select>
44+
<span id="meta"></span>
45+
</p>
46+
3847
<div id="out"></div>
3948

4049
<script type="module">
@@ -47,28 +56,58 @@ <h1>docling.rs → wasm: scanned PDF / image, full lite profile</h1>
4756

4857
const LAYOUT_URL =
4958
"https://github.com/docling-project/docling.rs/releases/download/models-v1/layout_heron_int8.onnx";
50-
const REC_URL =
51-
"https://huggingface.co/SWHL/RapidOCR/resolve/main/PP-OCRv3/en_PP-OCRv3_rec_infer.onnx";
52-
const DICT_URL =
53-
"https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/main/ppocr/utils/en_dict.txt";
5459
const SCALE = 2.0; // px per PDF point — the native pipeline's RENDER_SCALE
5560

5661
const meta = document.getElementById("meta");
5762
const out = document.getElementById("out");
63+
const lang = document.getElementById("lang");
5864

5965
await init();
60-
meta.textContent = "fetching models (layout ~165 MB on first visit; browser-cached) …";
61-
const [layoutModel, recModel, dict] = await Promise.all([
62-
fetch(LAYOUT_URL, { cache: "force-cache" }).then((r) => r.arrayBuffer()),
63-
fetch(REC_URL, { cache: "force-cache" }).then((r) => r.arrayBuffer()),
64-
fetch(DICT_URL, { cache: "force-cache" }).then((r) => r.text()),
65-
]);
66+
const REC_MODELS = {
67+
en: {
68+
model: "https://huggingface.co/SWHL/RapidOCR/resolve/main/PP-OCRv3/en_PP-OCRv3_rec_infer.onnx",
69+
dict: "https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/main/ppocr/utils/en_dict.txt",
70+
},
71+
cyrillic: {
72+
model: "https://huggingface.co/SWHL/RapidOCR/resolve/main/PP-OCRv3/cyrillic_PP-OCRv3_rec_infer.onnx",
73+
dict: "https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/main/ppocr/utils/dict/cyrillic_dict.txt",
74+
},
75+
ch: {
76+
model: "https://huggingface.co/SWHL/RapidOCR/resolve/main/PP-OCRv3/ch_PP-OCRv3_rec_infer.onnx",
77+
dict: "https://raw.githubusercontent.com/PaddlePaddle/PaddleOCR/main/ppocr/utils/ppocr_keys_v1.txt",
78+
},
79+
};
80+
const recCache = {};
81+
async function recFor(lang) {
82+
if (!recCache[lang]) {
83+
meta.textContent = `fetching ${lang} recognition model (~10 MB, cached) ...`;
84+
const spec = REC_MODELS[lang];
85+
const [model, dict] = await Promise.all([
86+
fetch(spec.model, { cache: "force-cache" }).then((r) => r.arrayBuffer()),
87+
fetch(spec.dict, { cache: "force-cache" }).then((r) => r.text()),
88+
]);
89+
const session = await ort.InferenceSession.create(model, { executionProviders: ["wasm"] });
90+
recCache[lang] = {
91+
dict,
92+
rec: {
93+
run: async (n, h, w, data) => {
94+
const results = await session.run({
95+
[session.inputNames[0]]: new ort.Tensor("float32", data, [n, 3, h, w]),
96+
});
97+
const t = results[session.outputNames[0]];
98+
return { data: t.data, dims: Array.from(t.dims) };
99+
},
100+
},
101+
};
102+
}
103+
return recCache[lang];
104+
}
105+
meta.textContent = "fetching layout model (~165 MB on first visit; browser-cached) …";
106+
const layoutModel = await fetch(LAYOUT_URL, { cache: "force-cache" }).then((r) => r.arrayBuffer());
66107
const layoutSession = await ort.InferenceSession.create(layoutModel, {
67108
executionProviders: ["wasm"],
68109
});
69-
const recSession = await ort.InferenceSession.create(recModel, {
70-
executionProviders: ["wasm"],
71-
});
110+
await recFor(lang.value);
72111
meta.textContent = "ready — drop a scanned PDF or image";
73112

74113
// Interop wrappers docling_wasm expects (see src/scanned.rs / src/ocr.rs).
@@ -81,17 +120,9 @@ <h1>docling.rs → wasm: scanned PDF / image, full lite profile</h1>
81120
return { logits: t("logits"), boxes: t("pred_boxes") };
82121
},
83122
};
84-
const rec = {
85-
run: async (n, h, w, data) => {
86-
const results = await recSession.run({
87-
[recSession.inputNames[0]]: new ort.Tensor("float32", data, [n, 3, h, w]),
88-
});
89-
const t = results[recSession.outputNames[0]];
90-
return { data: t.data, dims: Array.from(t.dims) };
91-
},
92-
};
93123

94124
async function handlePdf(bytes, name) {
125+
const { dict, rec } = await recFor(lang.value);
95126
const pdf = await pdfjs.getDocument({ data: bytes }).promise;
96127
const conv = new ScannedConverter(dict);
97128
const canvas = document.createElement("canvas");
@@ -114,6 +145,7 @@ <h1>docling.rs → wasm: scanned PDF / image, full lite profile</h1>
114145
const started = performance.now();
115146
try {
116147
const bytes = new Uint8Array(await file.arrayBuffer());
148+
const { dict, rec } = await recFor(lang.value);
117149
const md = file.name.toLowerCase().endsWith(".pdf")
118150
? await handlePdf(bytes, file.name)
119151
: await convert_scanned_image(bytes, file.name, dict, layout, rec, "md");

0 commit comments

Comments
 (0)