hung-k-nguyen commited on
Commit
dccadc6
·
0 Parent(s):

Release snapshot

Browse files
.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ pipeline_tag: image-text-to-text
4
+ language: [multilingual]
5
+ tags:
6
+ - page-stream-segmentation
7
+ - document-boundary-detection
8
+ - document-ai
9
+ - document-splitting
10
+ - open-weights
11
+ datasets:
12
+ - nutrientdocs/doc-split-benchmark
13
+ ---
14
+
15
+ # doc-split-v1 — open-weight
16
+
17
+ **Where does one document end and the next begin?** An open-weight page-stream-segmentation model you can
18
+ download and run: it splits a stream of pages (a scanned batch / merged PDF) back into its constituent
19
+ documents.
20
+
21
+ The lightweight, open sibling of the commercial flagship
22
+ [`doc-split-v2`](https://huggingface.co/nutrientdocs/doc-split-v2) — compact, ~4.5× faster, near-flagship
23
+ accuracy on our data and multilingual out of the box. Shipped as **ONNX** — runs with `onnxruntime`, no
24
+ framework or modelling code to install.
25
+
26
+ - 🎯 **Try it:** [doc-split-demo](https://huggingface.co/spaces/nutrientdocs/doc-split-demo?model=v1)
27
+ - 🏆 **Leaderboard:** [doc-split-leaderboard](https://huggingface.co/spaces/nutrientdocs/doc-split-leaderboard)
28
+ - 📊 **Benchmark:** [doc-split-benchmark](https://huggingface.co/datasets/nutrientdocs/doc-split-benchmark)
29
+ - 🔒 **Higher accuracy?** [doc-split-v2](https://huggingface.co/nutrientdocs/doc-split-v2) (commercial)
30
+
31
+ ## Results — boundary F1 (κ)
32
+
33
+ Per-page boundary detection, page 0 forced. **This model** vs the private doc-split-v2, the strongest cloud VLM,
34
+ and prior work.
35
+
36
+ | Cut | **doc-split-v1** | doc-split-v2 | best cloud VLM | OpenPSS specialist |
37
+ |---|---|---|---|---|
38
+ | OpenPSS-short (sparse) | **0.585** (.53) | 0.619 | 0.598 (gemini-flash) | 0.76 |
39
+ | OpenPSS-long | **0.859** (.82) | 0.886 | 0.244 (gemini-flash) | 0.83 |
40
+ | our-200 (synthetic) | **0.936** (.78) | 0.934 | 0.942 (gpt-sol) | — |
41
+ | TABME++ test | **0.704** (.56) | 0.901 | — | — |
42
+ | Tobacco800 test | **0.820** (.60) | 0.957 | — | — |
43
+ | val (real-doc) | **0.918** (.86) | 0.908 | — | — |
44
+
45
+ Beats every evaluated cloud VLM on OpenPSS-**long** (0.859 vs 0.244) at a fraction of the cost, and holds up
46
+ on our data. TABME++/Tobacco800 are zero-shot for this model (in-domain for doc-split-v2).
47
+
48
+ ## What's in this repo
49
+
50
+ Runs entirely under `onnxruntime` — nothing else to install.
51
+
52
+ - `image_model.onnx`, `text_model.onnx` — the image and text towers (per-page embeddings).
53
+ - `head.onnx` — the boundary head (per-page boundary score).
54
+ - `crf.json` — smoothing parameters for the per-page confidence.
55
+ - `tokenizer.json` (+ config) — the bundled text tokenizer.
56
+
57
+ ## Usage (ONNX)
58
+
59
+ ```python
60
+ # pip install onnxruntime transformers numpy huggingface_hub
61
+ import numpy as np, onnxruntime as ort, json
62
+ from transformers import AutoTokenizer
63
+ from huggingface_hub import snapshot_download
64
+
65
+ d = snapshot_download("nutrientdocs/doc-split-v1")
66
+ img = ort.InferenceSession(f"{d}/image_model.onnx", providers=["CPUExecutionProvider"])
67
+ text = ort.InferenceSession(f"{d}/text_model.onnx", providers=["CPUExecutionProvider"])
68
+ head = ort.InferenceSession(f"{d}/head.onnx", providers=["CPUExecutionProvider"])
69
+ tok = AutoTokenizer.from_pretrained(d); crf = json.load(open(f"{d}/crf.json"))
70
+
71
+ def _lse(x, ax):
72
+ m = x.max(ax, keepdims=True); return (m + np.log(np.exp(x - m).sum(ax, keepdims=True))).squeeze(ax)
73
+
74
+ def marginals(bl, crf): # per-page confidence via forward-backward over a 2-tag chain
75
+ T = np.asarray(crf["trans"]); s = np.asarray(crf["start"]); e_ = np.asarray(crf["end"])
76
+ N = len(bl); e = np.stack([np.zeros(N), bl], 1); a = np.zeros((N, 2)); a[0] = s + e[0]
77
+ for t in range(1, N): a[t] = _lse(a[t-1][:, None] + T, 0) + e[t]
78
+ b = np.zeros((N, 2)); b[N-1] = e_
79
+ for t in range(N-2, -1, -1): b[t] = _lse(T + (e[t+1] + b[t+1])[None, :], 1)
80
+ m = a + b; m -= m.max(1, keepdims=True); p = np.exp(m); return (p / p.sum(1, keepdims=True))[:, 1]
81
+
82
+ def split(pages, tau=0.5): # pages: list of (PIL image, ocr_text or "")
83
+ arr = np.stack([(np.asarray(im.convert("RGB").resize((512, 512)), np.float32)/255 - .5)/.5
84
+ for im, _ in pages]).transpose(0, 3, 1, 2).astype(np.float32)
85
+ vi = img.run(["image_embed"], {"pixel_values": arr})[0]
86
+ b = tok(["query: "+(t or " ") for _, t in pages], padding=True, truncation=True,
87
+ max_length=512, return_tensors="np")
88
+ vt = text.run(["text_embed"], {"input_ids": b["input_ids"].astype(np.int64),
89
+ "attention_mask": b["attention_mask"].astype(np.int64)})[0]
90
+ g = np.array([1. if (t and t.strip()) else 0. for _, t in pages], np.float32); N = len(pages)
91
+ vt = vt * g[:, None] # OCR gate: text ignored on pages with no text layer
92
+ bl = head.run(["boundary_logit"], {"v_img": vi[None], "v_txt": vt[None],
93
+ "gate": g[None], "mask": np.ones((1, N), np.float32)})[0][0]
94
+ bl[0] = 30.0 # force page 0 to start a document
95
+ conf = marginals(bl, crf) # per-page confidence in [0,1]
96
+ return [1 if (i == 0 or conf[i] >= tau) else 0 for i in range(N)] # 1 = this page starts a new document
97
+ ```
98
+
99
+ ## Intended use & limits
100
+
101
+ **Use it for:** splitting merged/batch-scanned PDFs into documents; routing; pre-processing for
102
+ classification/extraction. **Limits:** boundary detection only (does not classify document *type*); the
103
+ sparse low-boundary regime (OpenPSS-short) is hardest; OCR text helps on text-heavy pages.
104
+
105
+ ## License
106
+
107
+ Apache-2.0.
108
+
109
+ ## Calibrated confidence
110
+
111
+ The raw boundary score is over-confident (a raw 0.85 is really ~63% likely a true boundary). We ship a
112
+ **beta calibration** (fit on held-out data) so the reported confidence is honest and usable as a threshold:
113
+
114
+ ```
115
+ p_calibrated = sigmoid(a·ln(p) + b·ln(1-p) + c), (a, b, c) = (0.516, -0.402, -0.155)
116
+ ```
117
+ ECE 0.044 → 0.012. The demo applies this and lets you set a minimum-confidence threshold on the calibrated value.
118
+
119
+ ## About the author
120
+
121
+ <a href="https://nutrient.io/">
122
+ <img src="https://avatars2.githubusercontent.com/u/1527679?v=3&s=200" height="80" />
123
+ </a>
124
+
125
+ This project is maintained and funded by [Nutrient](https://nutrient.io/) - The deterministic document infrastructure enterprises run their highest-stakes workflows on: replayable output, clear exceptions, and full audit trails on the messy, regulated documents where AI alone breaks.
crf.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "trans": [
3
+ [
4
+ 0.0455835722386837,
5
+ -0.07097849994897842
6
+ ],
7
+ [
8
+ -0.07442624121904373,
9
+ 0.0578848272562027
10
+ ]
11
+ ],
12
+ "start": [
13
+ 0.04109737277030945,
14
+ -0.041104089468717575
15
+ ],
16
+ "end": [
17
+ -0.0374150313436985,
18
+ 0.037414439022541046
19
+ ],
20
+ "d_img": 768,
21
+ "d_txt": 384
22
+ }
head.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:de1bef8d8ae1889b24e1f3121e95b59fe0a0d3c902fc16080b8447d99c33ab75
3
+ size 87054851
image_model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:655849d83877953e8001cfd1efa03c085caf31511ef0ccc8d31b765fe3e91e89
3
+ size 375659790
special_tokens_map.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": {
3
+ "content": "<s>",
4
+ "lstrip": false,
5
+ "normalized": false,
6
+ "rstrip": false,
7
+ "single_word": false
8
+ },
9
+ "cls_token": {
10
+ "content": "<s>",
11
+ "lstrip": false,
12
+ "normalized": false,
13
+ "rstrip": false,
14
+ "single_word": false
15
+ },
16
+ "eos_token": {
17
+ "content": "</s>",
18
+ "lstrip": false,
19
+ "normalized": false,
20
+ "rstrip": false,
21
+ "single_word": false
22
+ },
23
+ "mask_token": {
24
+ "content": "<mask>",
25
+ "lstrip": false,
26
+ "normalized": false,
27
+ "rstrip": false,
28
+ "single_word": false
29
+ },
30
+ "pad_token": {
31
+ "content": "<pad>",
32
+ "lstrip": false,
33
+ "normalized": false,
34
+ "rstrip": false,
35
+ "single_word": false
36
+ },
37
+ "sep_token": {
38
+ "content": "</s>",
39
+ "lstrip": false,
40
+ "normalized": false,
41
+ "rstrip": false,
42
+ "single_word": false
43
+ },
44
+ "unk_token": {
45
+ "content": "<unk>",
46
+ "lstrip": false,
47
+ "normalized": false,
48
+ "rstrip": false,
49
+ "single_word": false
50
+ }
51
+ }
text_model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9141a601b17b1aa38a502b2b02aae19b82b2969f2f1e44ed5ef1cf91ee6aa958
3
+ size 471476379
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cd98e5698b201ba914efb8c18b6709fa8735ab71dcad8d2b431e52e8bf68d932
3
+ size 17082800
tokenizer_config.json ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "<s>",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ },
11
+ "1": {
12
+ "content": "<pad>",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "2": {
20
+ "content": "</s>",
21
+ "lstrip": false,
22
+ "normalized": false,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": true
26
+ },
27
+ "3": {
28
+ "content": "<unk>",
29
+ "lstrip": false,
30
+ "normalized": false,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": true
34
+ },
35
+ "250001": {
36
+ "content": "<mask>",
37
+ "lstrip": false,
38
+ "normalized": false,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": true
42
+ }
43
+ },
44
+ "bos_token": "<s>",
45
+ "clean_up_tokenization_spaces": true,
46
+ "cls_token": "<s>",
47
+ "eos_token": "</s>",
48
+ "extra_special_tokens": {},
49
+ "mask_token": "<mask>",
50
+ "model_max_length": 512,
51
+ "pad_token": "<pad>",
52
+ "sep_token": "</s>",
53
+ "sp_model_kwargs": {},
54
+ "tokenizer_class": "XLMRobertaTokenizer",
55
+ "unk_token": "<unk>"
56
+ }