mathcraft-ocr 0.2.2__tar.gz → 0.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/LICENSE +21 -21
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/MANIFEST.in +13 -13
- {mathcraft_ocr-0.2.2/mathcraft_ocr.egg-info → mathcraft_ocr-0.2.5}/PKG-INFO +12 -4
- mathcraft_ocr-0.2.5/README.md +229 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/README_MATHCRAFT_OCR.md +11 -3
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/__init__.py +1 -1
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/text_recognizer.py +6 -4
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/cache.py +5 -1
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/formula_lines.py +94 -8
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/hardware.py +80 -2
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/manifests/models.v1.json +1 -1
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/providers.py +8 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/runtime.py +20 -4
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/serialization.py +2 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5/mathcraft_ocr.egg-info}/PKG-INFO +12 -4
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/SOURCES.txt +1 -1
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/pyproject.toml +3 -7
- mathcraft_ocr-0.2.2/readme.md +0 -389
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/__main__.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/__init__.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/common.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/formula_detector.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/formula_recognizer.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/text_detector.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/api.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/cli.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/debug_blocks.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/doctor.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/downloader.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/error_patterns.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/errors.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/image.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/latex_alignment.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/latex_quality.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/layout.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/manifest.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/profiles.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/results.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/worker.py +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/dependency_links.txt +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/entry_points.txt +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/requires.txt +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/top_level.txt +0 -0
- {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/setup.cfg +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 MathCraft
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MathCraft
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
include LICENSE
|
|
1
|
+
include LICENSE
|
|
2
2
|
include README_MATHCRAFT_OCR.md
|
|
3
|
-
include pyproject.toml
|
|
4
|
-
graft mathcraft_ocr
|
|
5
|
-
prune build
|
|
6
|
-
prune dist
|
|
7
|
-
prune docs
|
|
8
|
-
prune release_assets
|
|
9
|
-
prune scripts
|
|
10
|
-
prune src
|
|
11
|
-
prune test
|
|
12
|
-
prune test_pdf
|
|
13
|
-
global-exclude __pycache__
|
|
14
|
-
global-exclude *.py[cod]
|
|
3
|
+
include pyproject.toml
|
|
4
|
+
graft mathcraft_ocr
|
|
5
|
+
prune build
|
|
6
|
+
prune dist
|
|
7
|
+
prune docs
|
|
8
|
+
prune release_assets
|
|
9
|
+
prune scripts
|
|
10
|
+
prune src
|
|
11
|
+
prune test
|
|
12
|
+
prune test_pdf
|
|
13
|
+
global-exclude __pycache__
|
|
14
|
+
global-exclude *.py[cod]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mathcraft-ocr
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: ONNX-only OCR runtime for mathematical documents
|
|
5
5
|
Author: SakuraMathcraft
|
|
6
6
|
License-Expression: MIT
|
|
@@ -139,10 +139,12 @@ mathcraft worker --provider auto
|
|
|
139
139
|
|
|
140
140
|
## Model Cache
|
|
141
141
|
|
|
142
|
-
MathCraft reads models from:
|
|
142
|
+
MathCraft reads models from a platform-specific default user data root:
|
|
143
143
|
|
|
144
144
|
```text
|
|
145
|
-
%APPDATA%\MathCraft\models
|
|
145
|
+
Windows: %APPDATA%\MathCraft\models
|
|
146
|
+
macOS: ~/Library/Application Support/LaTeXSnipper/MathCraft/models
|
|
147
|
+
Linux: ${XDG_DATA_HOME:-~/.local/share}/LaTeXSnipper/MathCraft/models
|
|
146
148
|
```
|
|
147
149
|
|
|
148
150
|
or from a custom root:
|
|
@@ -169,7 +171,9 @@ mathcraft doctor --provider auto
|
|
|
169
171
|
Open a new PowerShell window after removing the persistent variable. The default root is:
|
|
170
172
|
|
|
171
173
|
```text
|
|
172
|
-
%APPDATA%\MathCraft\models
|
|
174
|
+
Windows: %APPDATA%\MathCraft\models
|
|
175
|
+
macOS: ~/Library/Application Support/LaTeXSnipper/MathCraft/models
|
|
176
|
+
Linux: ${XDG_DATA_HOME:-~/.local/share}/LaTeXSnipper/MathCraft/models
|
|
173
177
|
```
|
|
174
178
|
|
|
175
179
|
Model artifacts are downloaded from the MathCraft-Models release assets declared in `mathcraft_ocr/manifests/models.v1.json`.
|
|
@@ -192,6 +196,10 @@ Model artifacts are downloaded from the MathCraft-Models release assets declared
|
|
|
192
196
|
|
|
193
197
|
The actual provider is available on results through the `provider` field.
|
|
194
198
|
|
|
199
|
+
## Benchmarks
|
|
200
|
+
|
|
201
|
+
The active model set has checked-in results for UniMER-Test, MathWriting test, and OpenStax mixed pages. See the [published benchmark summary](https://github.com/SakuraMathcraft/MathCraft-Models/tree/main/benchmarks) and the [full reproducibility suite](https://github.com/SakuraMathcraft/LaTeXSnipper/tree/main/benchmarks/mathcraft_ocr).
|
|
202
|
+
|
|
195
203
|
## Development
|
|
196
204
|
|
|
197
205
|
Run tests from the repository root:
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# MathCraft Models
|
|
2
|
+
|
|
3
|
+
Model assets for **MathCraft OCR**, the ONNX-only OCR runtime used by LaTeXSnipper.
|
|
4
|
+
|
|
5
|
+
MathCraft OCR recognizes formulae, text, and mixed mathematical documents with a compact ONNX model set. This repository provides the model release assets and the source package for the PyPI package `mathcraft-ocr` used by LaTeXSnipper.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
Current PyPI release line: `mathcraft-ocr 0.2.5`.
|
|
10
|
+
|
|
11
|
+
Install the library and CLI without choosing an ONNX Runtime backend:
|
|
12
|
+
|
|
13
|
+
```powershell
|
|
14
|
+
pip install mathcraft-ocr
|
|
15
|
+
mathcraft --help
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Install exactly one ONNX Runtime backend before running OCR inference.
|
|
19
|
+
|
|
20
|
+
CPU:
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
pip install "mathcraft-ocr[cpu]"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
GPU:
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
pip install "mathcraft-ocr[gpu]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Use only one ONNX Runtime backend in the same environment. Do not install `onnxruntime` and `onnxruntime-gpu` together.
|
|
33
|
+
|
|
34
|
+
LaTeXSnipper's dependency wizard selects the ONNX Runtime GPU wheel line from the detected CUDA toolkit. CUDA 11.x uses the ONNX Runtime CUDA 11 package feed, CUDA 12.x uses the stable PyPI GPU wheels, and CUDA 13.x uses the ONNX Runtime CUDA 13 nightly feed. Static `mathcraft-ocr[gpu]` package metadata cannot inspect the local CUDA toolkit, so CUDA 11.x users installing manually should use the CUDA 11 feed shown by the wizard.
|
|
35
|
+
|
|
36
|
+
Upgrade the current release with a chosen backend:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
pip install -U "mathcraft-ocr[gpu]"
|
|
40
|
+
mathcraft --help
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Check the runtime:
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
mathcraft doctor --provider auto
|
|
47
|
+
mathcraft models check
|
|
48
|
+
mathcraft warmup --profile mixed --provider auto
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Recognize an image:
|
|
52
|
+
|
|
53
|
+
```powershell
|
|
54
|
+
mathcraft ocr "C:\path\to\formula.png" --profile formula --provider auto --json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Mixed OCR to Markdown:
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
mathcraft ocr "C:\path\to\page.png" --profile mixed --provider auto --output result.md
|
|
61
|
+
mathcraft ocr "C:\path\to\page.png" --profile mixed --provider auto --output-dir "D:\MathCraft\outputs"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
When a file is written, the CLI prints the resolved output path:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
[MATHCRAFT_OUTPUT] written to D:\MathCraft\outputs\page.md
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
PowerShell custom model cache:
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
$env:MATHCRAFT_HOME="D:\MathCraft\models"
|
|
74
|
+
mathcraft doctor --provider auto
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Persistent user-level cache path:
|
|
78
|
+
|
|
79
|
+
```powershell
|
|
80
|
+
setx MATHCRAFT_HOME "D:\MathCraft\models"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Open a new terminal after `setx`.
|
|
84
|
+
|
|
85
|
+
Restore the default cache path:
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
[Environment]::SetEnvironmentVariable("MATHCRAFT_HOME", $null, "User")
|
|
89
|
+
Remove-Item Env:\MATHCRAFT_HOME -ErrorAction SilentlyContinue
|
|
90
|
+
mathcraft doctor --provider auto
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Open a new terminal after removing the persistent variable. The default root is platform-specific:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
Windows: %APPDATA%\MathCraft\models
|
|
97
|
+
macOS: ~/Library/Application Support/LaTeXSnipper/MathCraft/models
|
|
98
|
+
Linux: ${XDG_DATA_HOME:-~/.local/share}/LaTeXSnipper/MathCraft/models
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Python API
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from mathcraft_ocr import MathCraftRuntime
|
|
105
|
+
|
|
106
|
+
runtime = MathCraftRuntime(provider_preference="auto")
|
|
107
|
+
result = runtime.recognize_mixed(r"C:\path\to\page.png")
|
|
108
|
+
|
|
109
|
+
print(result.text)
|
|
110
|
+
for block in result.blocks:
|
|
111
|
+
print(block.role, block.kind, block.text[:80])
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Profiles
|
|
115
|
+
|
|
116
|
+
| Profile | Use Case | Output |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `formula` | Formula screenshots | LaTeX formula text |
|
|
119
|
+
| `text` | Plain text OCR | Text |
|
|
120
|
+
| `mixed` | Text + formula documents | Markdown-ready structured text |
|
|
121
|
+
|
|
122
|
+
## Runtime Release Notes
|
|
123
|
+
|
|
124
|
+
`mathcraft-ocr 0.2.5` improves multiline formula recognition by retaining aligned short continuation rows, excluding dark screenshot frames, and comparing uncertain wide-line segments with whole-line recognition. The `v1.0.0` formula-recognition asset now uses the MathCraft-owned `mathcraft-formula-rec` identity in its configuration; the ONNX graphs and weights are unchanged.
|
|
125
|
+
|
|
126
|
+
`mathcraft-ocr 0.2.4` fixes DirectML provider handling for RapidOCR text recognition. CUDA and TensorRT providers enable CUDA runtime options, DirectML enables DirectML runtime options, and CPU remains CPU-only. The active `v1.0.0` ONNX model asset set is unchanged.
|
|
127
|
+
|
|
128
|
+
`mathcraft-ocr 0.2.3` fixes cross-platform hardware sizing. Memory detection now uses optional `psutil`, Windows API, POSIX `sysconf`, and macOS `vm_stat`, so CPU batch sizing no longer falls back to Windows-only memory data on Linux or macOS. It also moves the default writable model cache to platform-native user data locations on macOS and Linux.
|
|
129
|
+
|
|
130
|
+
Earlier `0.2.x` releases improved runtime-side formula post-processing without changing the active `v1.0.0` ONNX model asset set. They keep compact fraction expressions whole, avoid splitting matrix-like wide formulas, add relation-aware `aligned` output, and retry severe segmented-line artifacts with safer whole-line or whole-image recognition.
|
|
131
|
+
|
|
132
|
+
## Model Set
|
|
133
|
+
|
|
134
|
+
Active release: `v1.0.0`
|
|
135
|
+
|
|
136
|
+
| Model ID | Runtime | Purpose |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| `mathcraft-formula-det` | ONNX | Mathematical formula region detection |
|
|
139
|
+
| `mathcraft-formula-rec` | ONNX | Formula-to-LaTeX recognition |
|
|
140
|
+
| `mathcraft-text-det` | ONNX | Fast multilingual text detection |
|
|
141
|
+
| `mathcraft-text-rec` | ONNX | Fast multilingual text recognition |
|
|
142
|
+
|
|
143
|
+
Release assets:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
mathcraft-formula-det.zip
|
|
147
|
+
mathcraft-formula-rec.zip
|
|
148
|
+
mathcraft-text-det.zip
|
|
149
|
+
mathcraft-text-rec.zip
|
|
150
|
+
models.v1.json
|
|
151
|
+
SHA256SUMS.txt
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The runtime downloads the four model archives. `models.v1.json` records their
|
|
155
|
+
contents and source URLs, while `SHA256SUMS.txt` provides archive-level integrity
|
|
156
|
+
checks for release verification.
|
|
157
|
+
|
|
158
|
+
Default writable model root:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
Windows: %APPDATA%\MathCraft\models
|
|
162
|
+
macOS: ~/Library/Application Support/LaTeXSnipper/MathCraft/models
|
|
163
|
+
Linux: ${XDG_DATA_HOME:-~/.local/share}/LaTeXSnipper/MathCraft/models
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The runtime checks the manifest before initialization. Missing or incomplete model folders are repaired automatically by downloading only the affected model asset.
|
|
167
|
+
|
|
168
|
+
Interrupted downloads are resumable. Partial archives are stored under the active writable model root:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
<MATHCRAFT_HOME>\.downloads\<model_id>.zip.part
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
After a model archive is fully downloaded, verified, and extracted, the `.part` file is removed automatically.
|
|
175
|
+
|
|
176
|
+
## Results
|
|
177
|
+
|
|
178
|
+
The examples below are generated from MathCraft's structured block output. Boxes show detected roles, order, column metadata, score, and layout flags.
|
|
179
|
+
|
|
180
|
+
### Abstract Algebra, page 18
|
|
181
|
+
|
|
182
|
+
Formula-heavy English mathematical prose with dense inline and display formulae.
|
|
183
|
+
|
|
184
|
+
<img width="1700" height="2200" alt="debug_blocks" src="https://github.com/user-attachments/assets/855d0694-4068-4e32-ac5e-80f0c4178332" />
|
|
185
|
+
|
|
186
|
+
### Dynamics journal, page 5
|
|
187
|
+
|
|
188
|
+
Formula-dominant journal page with display equations, anchors, labels, headers, and page numbers.
|
|
189
|
+
|
|
190
|
+
<img width="1700" height="2200" alt="debug_blocks" src="https://github.com/user-attachments/assets/b1498ee9-32fd-4d56-bbfb-876881758587" />
|
|
191
|
+
|
|
192
|
+
### Chinese lecture note, page 1
|
|
193
|
+
|
|
194
|
+
Chinese mathematical document page with mixed text and formula blocks.
|
|
195
|
+
|
|
196
|
+
<img width="1654" height="2339" alt="debug_blocks" src="https://github.com/user-attachments/assets/5d153b2d-d089-4ee2-9ee7-c7f52eb099c9" />
|
|
197
|
+
|
|
198
|
+
### Limits and series, page 1
|
|
199
|
+
|
|
200
|
+
Sparse title/cover-style page used to check layout stability.
|
|
201
|
+
|
|
202
|
+
<img width="1221" height="1898" alt="debug_blocks" src="https://github.com/user-attachments/assets/6c6404e0-bea4-4811-b135-feff3a063a18" />
|
|
203
|
+
|
|
204
|
+
## Reproducible Benchmarks
|
|
205
|
+
|
|
206
|
+
| Benchmark | Scale | Reported result |
|
|
207
|
+
| --- | ---: | --- |
|
|
208
|
+
| UniMER-Test | 23,757 formulas | BLEU-4 `0.7946`; official CDM `0.9288` |
|
|
209
|
+
| MathWriting test | 7,644 samples | BLEU-4 `0.5467`; official CDM `0.750`; render success `98.63%` |
|
|
210
|
+
| OpenStax mixed pages | 200 pages | `0` failures, `0` empty outputs; median `6.65 s/page` |
|
|
211
|
+
|
|
212
|
+
All recorded runs used `CUDAExecutionProvider`. The datasets cover different tasks and protocols, so the rows are not a model-ranking comparison.
|
|
213
|
+
|
|
214
|
+

|
|
215
|
+
|
|
216
|
+
See the [benchmark report, charts, provenance, and reproduction notes](benchmarks/README.md).
|
|
217
|
+
|
|
218
|
+
## Why It Is Stable
|
|
219
|
+
|
|
220
|
+
- ONNX Runtime only, no active PyTorch inference dependency.
|
|
221
|
+
- Stable MathCraft-owned model IDs and folders.
|
|
222
|
+
- Manifest-based file checks and cache repair.
|
|
223
|
+
- Resumable model downloads for slow or interrupted networks.
|
|
224
|
+
- Formula detection before text OCR.
|
|
225
|
+
- Structured blocks for headings, paragraphs, display formulae, headers, page numbers, and columns.
|
|
226
|
+
|
|
227
|
+
## LaTeXSnipper
|
|
228
|
+
|
|
229
|
+
LaTeXSnipper already integrates MathCraft OCR. Normal users do not need to install this package manually.
|
|
@@ -93,10 +93,12 @@ mathcraft worker --provider auto
|
|
|
93
93
|
|
|
94
94
|
## Model Cache
|
|
95
95
|
|
|
96
|
-
MathCraft reads models from:
|
|
96
|
+
MathCraft reads models from a platform-specific default user data root:
|
|
97
97
|
|
|
98
98
|
```text
|
|
99
|
-
%APPDATA%\MathCraft\models
|
|
99
|
+
Windows: %APPDATA%\MathCraft\models
|
|
100
|
+
macOS: ~/Library/Application Support/LaTeXSnipper/MathCraft/models
|
|
101
|
+
Linux: ${XDG_DATA_HOME:-~/.local/share}/LaTeXSnipper/MathCraft/models
|
|
100
102
|
```
|
|
101
103
|
|
|
102
104
|
or from a custom root:
|
|
@@ -123,7 +125,9 @@ mathcraft doctor --provider auto
|
|
|
123
125
|
Open a new PowerShell window after removing the persistent variable. The default root is:
|
|
124
126
|
|
|
125
127
|
```text
|
|
126
|
-
%APPDATA%\MathCraft\models
|
|
128
|
+
Windows: %APPDATA%\MathCraft\models
|
|
129
|
+
macOS: ~/Library/Application Support/LaTeXSnipper/MathCraft/models
|
|
130
|
+
Linux: ${XDG_DATA_HOME:-~/.local/share}/LaTeXSnipper/MathCraft/models
|
|
127
131
|
```
|
|
128
132
|
|
|
129
133
|
Model artifacts are downloaded from the MathCraft-Models release assets declared in `mathcraft_ocr/manifests/models.v1.json`.
|
|
@@ -146,6 +150,10 @@ Model artifacts are downloaded from the MathCraft-Models release assets declared
|
|
|
146
150
|
|
|
147
151
|
The actual provider is available on results through the `provider` field.
|
|
148
152
|
|
|
153
|
+
## Benchmarks
|
|
154
|
+
|
|
155
|
+
The active model set has checked-in results for UniMER-Test, MathWriting test, and OpenStax mixed pages. See the [published benchmark summary](https://github.com/SakuraMathcraft/MathCraft-Models/tree/main/benchmarks) and the [full reproducibility suite](https://github.com/SakuraMathcraft/LaTeXSnipper/tree/main/benchmarks/mathcraft_ocr).
|
|
156
|
+
|
|
149
157
|
## Development
|
|
150
158
|
|
|
151
159
|
Run tests from the repository root:
|
|
@@ -54,12 +54,14 @@ def recognize_pp_text_lines(
|
|
|
54
54
|
|
|
55
55
|
def _create_pp_text_recognizer(model_dir: Path, provider_info) -> TextRecognizer:
|
|
56
56
|
model_dir = model_dir.resolve()
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
active_provider = str(getattr(provider_info, "active_provider", "") or "")
|
|
58
|
+
use_cuda = active_provider in {"CUDAExecutionProvider", "TensorrtExecutionProvider"}
|
|
59
|
+
use_dml = active_provider == "DmlExecutionProvider"
|
|
60
|
+
return _create_pp_text_recognizer_cached(str(model_dir), use_cuda, use_dml)
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
@lru_cache(maxsize=8)
|
|
62
|
-
def _create_pp_text_recognizer_cached(model_dir: str, use_cuda: bool) -> TextRecognizer:
|
|
64
|
+
def _create_pp_text_recognizer_cached(model_dir: str, use_cuda: bool, use_dml: bool) -> TextRecognizer:
|
|
63
65
|
model_dir = Path(model_dir)
|
|
64
66
|
model_candidates = sorted(model_dir.glob("**/*rec*.onnx"))
|
|
65
67
|
if not model_candidates:
|
|
@@ -96,7 +98,7 @@ def _create_pp_text_recognizer_cached(model_dir: str, use_cuda: bool) -> TextRec
|
|
|
96
98
|
"cudnn_conv_algo_search": "EXHAUSTIVE",
|
|
97
99
|
"do_copy_in_default_stream": True,
|
|
98
100
|
},
|
|
99
|
-
"use_dml":
|
|
101
|
+
"use_dml": use_dml,
|
|
100
102
|
"dm_ep_cfg": None,
|
|
101
103
|
"use_cann": False,
|
|
102
104
|
"cann_ep_cfg": {
|
|
@@ -14,7 +14,11 @@ def default_user_models_dir() -> Path:
|
|
|
14
14
|
appdata = os.environ.get("APPDATA", "")
|
|
15
15
|
if appdata:
|
|
16
16
|
return Path(appdata) / "MathCraft" / "models"
|
|
17
|
-
|
|
17
|
+
if sys.platform == "darwin":
|
|
18
|
+
return Path.home() / "Library" / "Application Support" / "LaTeXSnipper" / "MathCraft" / "models"
|
|
19
|
+
xdg_data_home = os.environ.get("XDG_DATA_HOME", "").strip()
|
|
20
|
+
data_root = Path(xdg_data_home) if xdg_data_home else Path.home() / ".local" / "share"
|
|
21
|
+
return data_root / "LaTeXSnipper" / "MathCraft" / "models"
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
def resolve_user_models_dir(cache_dir: str | Path | None = None) -> Path:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections import deque
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
import re
|
|
7
8
|
|
|
@@ -113,6 +114,7 @@ def _ink_mask(rgb: np.ndarray) -> np.ndarray:
|
|
|
113
114
|
background = float(np.percentile(gray, 95))
|
|
114
115
|
threshold = min(245.0, max(80.0, background - 28.0))
|
|
115
116
|
mask = gray < threshold
|
|
117
|
+
mask = _remove_dark_outer_frame(mask)
|
|
116
118
|
|
|
117
119
|
height, width = mask.shape
|
|
118
120
|
border = max(1, min(height, width) // 80)
|
|
@@ -124,6 +126,50 @@ def _ink_mask(rgb: np.ndarray) -> np.ndarray:
|
|
|
124
126
|
return mask
|
|
125
127
|
|
|
126
128
|
|
|
129
|
+
def _remove_dark_outer_frame(mask: np.ndarray) -> np.ndarray:
|
|
130
|
+
height, width = mask.shape
|
|
131
|
+
corner_size = max(4, min(height, width) // 30)
|
|
132
|
+
corners = (
|
|
133
|
+
mask[:corner_size, :corner_size],
|
|
134
|
+
mask[:corner_size, -corner_size:],
|
|
135
|
+
mask[-corner_size:, :corner_size],
|
|
136
|
+
mask[-corner_size:, -corner_size:],
|
|
137
|
+
)
|
|
138
|
+
dark_corners = sum(float(corner.mean()) >= 0.25 for corner in corners)
|
|
139
|
+
if dark_corners < 3:
|
|
140
|
+
return mask
|
|
141
|
+
|
|
142
|
+
connected = np.zeros_like(mask, dtype=bool)
|
|
143
|
+
pending: deque[tuple[int, int]] = deque()
|
|
144
|
+
for y_start, x_start in (
|
|
145
|
+
(0, 0),
|
|
146
|
+
(0, width - corner_size),
|
|
147
|
+
(height - corner_size, 0),
|
|
148
|
+
(height - corner_size, width - corner_size),
|
|
149
|
+
):
|
|
150
|
+
points = np.argwhere(mask[y_start : y_start + corner_size, x_start : x_start + corner_size])
|
|
151
|
+
for y_offset, x_offset in points:
|
|
152
|
+
y = y_start + int(y_offset)
|
|
153
|
+
x = x_start + int(x_offset)
|
|
154
|
+
if connected[y, x]:
|
|
155
|
+
continue
|
|
156
|
+
connected[y, x] = True
|
|
157
|
+
pending.append((y, x))
|
|
158
|
+
|
|
159
|
+
while pending:
|
|
160
|
+
y, x = pending.popleft()
|
|
161
|
+
for next_y in range(max(0, y - 1), min(height, y + 2)):
|
|
162
|
+
for next_x in range(max(0, x - 1), min(width, x + 2)):
|
|
163
|
+
if connected[next_y, next_x] or not mask[next_y, next_x]:
|
|
164
|
+
continue
|
|
165
|
+
connected[next_y, next_x] = True
|
|
166
|
+
pending.append((next_y, next_x))
|
|
167
|
+
|
|
168
|
+
if int(connected.sum()) < max(32, int(round(mask.size * 0.0005))):
|
|
169
|
+
return mask
|
|
170
|
+
return mask & ~connected
|
|
171
|
+
|
|
172
|
+
|
|
127
173
|
def _row_bands(row_has_ink: np.ndarray) -> list[tuple[int, int]]:
|
|
128
174
|
bands: list[tuple[int, int]] = []
|
|
129
175
|
start: int | None = None
|
|
@@ -169,16 +215,14 @@ def _band_looks_like_formula_row(
|
|
|
169
215
|
return False
|
|
170
216
|
|
|
171
217
|
active_columns = int(np.count_nonzero(band.any(axis=0)))
|
|
172
|
-
if active_columns < max(
|
|
218
|
+
if active_columns < max(12, int(round(image_width * 0.015))):
|
|
173
219
|
return False
|
|
174
220
|
|
|
175
221
|
points = np.argwhere(band)
|
|
176
222
|
if points.size == 0:
|
|
177
223
|
return False
|
|
178
224
|
x_span = int(points[:, 1].max() - points[:, 1].min() + 1)
|
|
179
|
-
|
|
180
|
-
return active_columns >= max(28, int(round(image_width * 0.08)))
|
|
181
|
-
return x_span >= int(round(image_width * 0.22))
|
|
225
|
+
return x_span >= 24
|
|
182
226
|
|
|
183
227
|
|
|
184
228
|
def _filter_annotation_bands(
|
|
@@ -189,10 +233,44 @@ def _filter_annotation_bands(
|
|
|
189
233
|
) -> list[tuple[int, int]]:
|
|
190
234
|
if len(bands) < 2:
|
|
191
235
|
return bands
|
|
192
|
-
stats = [
|
|
193
|
-
|
|
236
|
+
stats = []
|
|
237
|
+
for top, bottom in bands:
|
|
238
|
+
points = np.argwhere(mask[top : bottom + 1, :])
|
|
239
|
+
if points.size == 0:
|
|
240
|
+
continue
|
|
241
|
+
stats.append(
|
|
242
|
+
(
|
|
243
|
+
_band_active_columns(mask, top, bottom),
|
|
244
|
+
int(points[:, 1].min()),
|
|
245
|
+
bottom - top + 1,
|
|
246
|
+
top,
|
|
247
|
+
bottom,
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
if len(stats) < 2:
|
|
251
|
+
return [(top, bottom) for _active, _left, _height, top, bottom in stats]
|
|
252
|
+
|
|
253
|
+
max_active = max(active for active, _left, _height, _top, _bottom in stats)
|
|
194
254
|
min_active = max(int(round(image_width * 0.13)), int(round(max_active * 0.35)))
|
|
195
|
-
|
|
255
|
+
strong = [stat for stat in stats if stat[0] >= min_active]
|
|
256
|
+
if not strong:
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
strong_heights = sorted(height for _active, _left, height, _top, _bottom in strong)
|
|
260
|
+
median_height = strong_heights[len(strong_heights) // 2]
|
|
261
|
+
aligned_tolerance = max(10, int(round(image_width * 0.03)))
|
|
262
|
+
minimum_context_height = max(5, int(round(median_height * 0.40)))
|
|
263
|
+
strong_lefts = [left for _active, left, _height, _top, _bottom in strong]
|
|
264
|
+
|
|
265
|
+
kept: list[tuple[int, int]] = []
|
|
266
|
+
for active, left, height, top, bottom in stats:
|
|
267
|
+
if active >= min_active:
|
|
268
|
+
kept.append((top, bottom))
|
|
269
|
+
continue
|
|
270
|
+
left_aligned = any(abs(left - strong_left) <= aligned_tolerance for strong_left in strong_lefts)
|
|
271
|
+
if height >= minimum_context_height and left_aligned:
|
|
272
|
+
kept.append((top, bottom))
|
|
273
|
+
return kept
|
|
196
274
|
|
|
197
275
|
|
|
198
276
|
def _looks_like_compact_fraction_split(
|
|
@@ -226,7 +304,15 @@ def _looks_like_compact_fraction_split(
|
|
|
226
304
|
first_width <= image_width * 0.86 and first_inset >= image_width * 0.06
|
|
227
305
|
)
|
|
228
306
|
second_is_wider = second_width >= first_width * 1.15 and second_inset <= image_width * 0.08
|
|
229
|
-
|
|
307
|
+
centers_are_close = abs(
|
|
308
|
+
(first_left + first_right) - (second_left + second_right)
|
|
309
|
+
) <= image_width * 0.12
|
|
310
|
+
widths_are_comparable = (
|
|
311
|
+
second_width >= first_width * 0.90
|
|
312
|
+
and second_inset >= image_width * 0.04
|
|
313
|
+
and centers_are_close
|
|
314
|
+
)
|
|
315
|
+
return first_is_centered_fragment and (second_is_wider or widths_are_comparable)
|
|
230
316
|
|
|
231
317
|
|
|
232
318
|
def _band_bounds(mask: np.ndarray, top: int, bottom: int) -> tuple[int, int, int, int] | None:
|