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.
Files changed (44) hide show
  1. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/LICENSE +21 -21
  2. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/MANIFEST.in +13 -13
  3. {mathcraft_ocr-0.2.2/mathcraft_ocr.egg-info → mathcraft_ocr-0.2.5}/PKG-INFO +12 -4
  4. mathcraft_ocr-0.2.5/README.md +229 -0
  5. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/README_MATHCRAFT_OCR.md +11 -3
  6. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/__init__.py +1 -1
  7. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/text_recognizer.py +6 -4
  8. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/cache.py +5 -1
  9. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/formula_lines.py +94 -8
  10. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/hardware.py +80 -2
  11. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/manifests/models.v1.json +1 -1
  12. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/providers.py +8 -0
  13. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/runtime.py +20 -4
  14. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/serialization.py +2 -0
  15. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5/mathcraft_ocr.egg-info}/PKG-INFO +12 -4
  16. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/SOURCES.txt +1 -1
  17. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/pyproject.toml +3 -7
  18. mathcraft_ocr-0.2.2/readme.md +0 -389
  19. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/__main__.py +0 -0
  20. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/__init__.py +0 -0
  21. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/common.py +0 -0
  22. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/formula_detector.py +0 -0
  23. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/formula_recognizer.py +0 -0
  24. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/text_detector.py +0 -0
  25. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/api.py +0 -0
  26. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/cli.py +0 -0
  27. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/debug_blocks.py +0 -0
  28. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/doctor.py +0 -0
  29. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/downloader.py +0 -0
  30. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/error_patterns.py +0 -0
  31. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/errors.py +0 -0
  32. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/image.py +0 -0
  33. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/latex_alignment.py +0 -0
  34. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/latex_quality.py +0 -0
  35. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/layout.py +0 -0
  36. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/manifest.py +0 -0
  37. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/profiles.py +0 -0
  38. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/results.py +0 -0
  39. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr/worker.py +0 -0
  40. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/dependency_links.txt +0 -0
  41. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/entry_points.txt +0 -0
  42. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/requires.txt +0 -0
  43. {mathcraft_ocr-0.2.2 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/top_level.txt +0 -0
  44. {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.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
+ ![Published-context UniMER-Test BLEU comparison](benchmarks/figures/unimer_public_bleu_comparison.png)
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:
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.2.2"
5
+ __version__ = "0.2.5"
6
6
 
7
7
  __all__ = [
8
8
  "DoctorReport",
@@ -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
- use_cuda = bool(getattr(provider_info, "device", "") == "gpu")
58
- return _create_pp_text_recognizer_cached(str(model_dir), use_cuda)
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": False,
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
- return Path.home() / ".mathcraft" / "models"
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(28, int(round(image_width * 0.13))):
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
- if x_span >= int(round(image_width * 0.55)):
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 = [(_band_active_columns(mask, top, bottom), top, bottom) for top, bottom in bands]
193
- max_active = max(active for active, _top, _bottom in stats)
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
- return [(top, bottom) for active, top, bottom in stats if active >= min_active]
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
- return first_is_centered_fragment and second_is_wider
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: