mathcraft-ocr 0.2.3__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.3/mathcraft_ocr.egg-info → mathcraft_ocr-0.2.5}/PKG-INFO +5 -1
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/README.md +20 -21
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/README_MATHCRAFT_OCR.md +4 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/__init__.py +1 -1
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/text_recognizer.py +6 -4
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/formula_lines.py +94 -8
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/manifests/models.v1.json +1 -1
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/providers.py +8 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/runtime.py +20 -4
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/serialization.py +2 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5/mathcraft_ocr.egg-info}/PKG-INFO +5 -1
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/pyproject.toml +1 -1
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/LICENSE +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/MANIFEST.in +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/__main__.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/__init__.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/common.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/formula_detector.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/formula_recognizer.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/text_detector.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/api.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/cache.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/cli.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/debug_blocks.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/doctor.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/downloader.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/error_patterns.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/errors.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/hardware.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/image.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/latex_alignment.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/latex_quality.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/layout.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/manifest.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/profiles.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/results.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/worker.py +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/SOURCES.txt +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/dependency_links.txt +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/entry_points.txt +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/requires.txt +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/top_level.txt +0 -0
- {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/setup.cfg +0 -0
|
@@ -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
|
|
@@ -196,6 +196,10 @@ Model artifacts are downloaded from the MathCraft-Models release assets declared
|
|
|
196
196
|
|
|
197
197
|
The actual provider is available on results through the `provider` field.
|
|
198
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
|
+
|
|
199
203
|
## Development
|
|
200
204
|
|
|
201
205
|
Run tests from the repository root:
|
|
@@ -6,7 +6,7 @@ MathCraft OCR recognizes formulae, text, and mixed mathematical documents with a
|
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
|
-
Current PyPI release line: `mathcraft-ocr 0.2.
|
|
9
|
+
Current PyPI release line: `mathcraft-ocr 0.2.5`.
|
|
10
10
|
|
|
11
11
|
Install the library and CLI without choosing an ONNX Runtime backend:
|
|
12
12
|
|
|
@@ -121,11 +121,13 @@ for block in result.blocks:
|
|
|
121
121
|
|
|
122
122
|
## Runtime Release Notes
|
|
123
123
|
|
|
124
|
-
`mathcraft-ocr 0.2.
|
|
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
125
|
|
|
126
|
-
|
|
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.
|
|
127
129
|
|
|
128
|
-
Earlier `0.2.x` releases
|
|
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.
|
|
129
131
|
|
|
130
132
|
## Model Set
|
|
131
133
|
|
|
@@ -145,9 +147,14 @@ mathcraft-formula-det.zip
|
|
|
145
147
|
mathcraft-formula-rec.zip
|
|
146
148
|
mathcraft-text-det.zip
|
|
147
149
|
mathcraft-text-rec.zip
|
|
150
|
+
models.v1.json
|
|
148
151
|
SHA256SUMS.txt
|
|
149
152
|
```
|
|
150
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
|
+
|
|
151
158
|
Default writable model root:
|
|
152
159
|
|
|
153
160
|
```text
|
|
@@ -194,27 +201,19 @@ Sparse title/cover-style page used to check layout stability.
|
|
|
194
201
|
|
|
195
202
|
<img width="1221" height="1898" alt="debug_blocks" src="https://github.com/user-attachments/assets/6c6404e0-bea4-4811-b135-feff3a063a18" />
|
|
196
203
|
|
|
197
|
-
##
|
|
204
|
+
## Reproducible Benchmarks
|
|
198
205
|
|
|
199
|
-
|
|
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` |
|
|
200
211
|
|
|
201
|
-
|
|
202
|
-
| --- | ---: |
|
|
203
|
-
| Pages | 10 |
|
|
204
|
-
| Total blocks | 495 |
|
|
205
|
-
| Text characters | 21,417 |
|
|
206
|
-
| Markdown lines | 304 |
|
|
207
|
-
| Mean page time | 8.34 s |
|
|
208
|
-
| Fastest page | 1.33 s |
|
|
209
|
-
| Slowest page | 18.53 s |
|
|
212
|
+
All recorded runs used `CUDAExecutionProvider`. The datasets cover different tasks and protocols, so the rows are not a model-ranking comparison.
|
|
210
213
|
|
|
211
|
-
|
|
214
|
+

|
|
212
215
|
|
|
213
|
-
|
|
214
|
-
Provider: CUDAExecutionProvider
|
|
215
|
-
Runtime: MathCraft OCR v1
|
|
216
|
-
Backend: ONNX Runtime
|
|
217
|
-
```
|
|
216
|
+
See the [benchmark report, charts, provenance, and reproduction notes](benchmarks/README.md).
|
|
218
217
|
|
|
219
218
|
## Why It Is Stable
|
|
220
219
|
|
|
@@ -150,6 +150,10 @@ Model artifacts are downloaded from the MathCraft-Models release assets declared
|
|
|
150
150
|
|
|
151
151
|
The actual provider is available on results through the `provider` field.
|
|
152
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
|
+
|
|
153
157
|
## Development
|
|
154
158
|
|
|
155
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": {
|
|
@@ -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:
|
|
@@ -25,6 +25,14 @@ class ProviderInfo:
|
|
|
25
25
|
gpu_runtime_ok: bool
|
|
26
26
|
cpu_fallback: bool
|
|
27
27
|
|
|
28
|
+
@property
|
|
29
|
+
def use_cuda(self) -> bool:
|
|
30
|
+
return self.active_provider in {"CUDAExecutionProvider", "TensorrtExecutionProvider"}
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def use_dml(self) -> bool:
|
|
34
|
+
return self.active_provider == "DmlExecutionProvider"
|
|
35
|
+
|
|
28
36
|
|
|
29
37
|
def detect_providers(prefer: str = "auto") -> ProviderInfo:
|
|
30
38
|
prefer_norm = (prefer or "auto").strip().lower()
|
|
@@ -81,6 +81,8 @@ ONNX_WARMUP_HANDLERS = {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
FORMULA_MAX_NEW_TOKENS = 512
|
|
84
|
+
_LOW_SEGMENT_CONFIDENCE = 0.95
|
|
85
|
+
_WHOLE_LINE_CONFIDENCE_MARGIN = 0.02
|
|
84
86
|
|
|
85
87
|
|
|
86
88
|
class MathCraftRuntime:
|
|
@@ -482,7 +484,7 @@ class MathCraftRuntime:
|
|
|
482
484
|
count = len(line_group.crops)
|
|
483
485
|
grouped_results.append(flat_results[offset : offset + count])
|
|
484
486
|
offset += count
|
|
485
|
-
grouped_results =
|
|
487
|
+
grouped_results = _repair_uncertain_segmented_lines(
|
|
486
488
|
rgb,
|
|
487
489
|
line_groups,
|
|
488
490
|
grouped_results,
|
|
@@ -621,7 +623,7 @@ def _merge_formula_group_results(results: list[list[tuple[str, float]]]) -> tupl
|
|
|
621
623
|
return text, score
|
|
622
624
|
|
|
623
625
|
|
|
624
|
-
def
|
|
626
|
+
def _repair_uncertain_segmented_lines(
|
|
625
627
|
rgb,
|
|
626
628
|
line_groups,
|
|
627
629
|
grouped_results: list[list[tuple[str, float]]],
|
|
@@ -635,7 +637,10 @@ def _repair_severe_segmented_lines(
|
|
|
635
637
|
if len(line_group.crops) <= 1:
|
|
636
638
|
continue
|
|
637
639
|
line_text = compose_formula_line([text for text, _score in line_results])
|
|
638
|
-
|
|
640
|
+
has_low_confidence = any(
|
|
641
|
+
score < _LOW_SEGMENT_CONFIDENCE for _text, score in line_results
|
|
642
|
+
)
|
|
643
|
+
if not has_low_confidence and not has_severe_latex_quality_issue(line_text):
|
|
639
644
|
continue
|
|
640
645
|
fallback_text, fallback_score = recognize_formula_image(
|
|
641
646
|
_crop_formula_line_group(rgb, line_group),
|
|
@@ -644,7 +649,18 @@ def _repair_severe_segmented_lines(
|
|
|
644
649
|
max_new_tokens=max_new_tokens,
|
|
645
650
|
)
|
|
646
651
|
line_score = _mean_score(line_results)
|
|
647
|
-
|
|
652
|
+
fallback_is_cleaner = _prefer_formula_fallback(
|
|
653
|
+
line_text,
|
|
654
|
+
line_score,
|
|
655
|
+
fallback_text,
|
|
656
|
+
fallback_score,
|
|
657
|
+
)
|
|
658
|
+
fallback_is_more_confident = (
|
|
659
|
+
has_low_confidence
|
|
660
|
+
and not has_severe_latex_quality_issue(fallback_text)
|
|
661
|
+
and fallback_score > line_score + _WHOLE_LINE_CONFIDENCE_MARGIN
|
|
662
|
+
)
|
|
663
|
+
if fallback_is_cleaner or fallback_is_more_confident:
|
|
648
664
|
repaired[index] = [(fallback_text, fallback_score)]
|
|
649
665
|
return repaired
|
|
650
666
|
|
|
@@ -72,6 +72,8 @@ def provider_info_to_json(provider_info) -> dict:
|
|
|
72
72
|
"gpu_requested": provider_info.gpu_requested,
|
|
73
73
|
"gpu_runtime_ok": provider_info.gpu_runtime_ok,
|
|
74
74
|
"cpu_fallback": provider_info.cpu_fallback,
|
|
75
|
+
"use_cuda": provider_info.use_cuda,
|
|
76
|
+
"use_dml": provider_info.use_dml,
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
|
|
@@ -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
|
|
@@ -196,6 +196,10 @@ Model artifacts are downloaded from the MathCraft-Models release assets declared
|
|
|
196
196
|
|
|
197
197
|
The actual provider is available on results through the `provider` field.
|
|
198
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
|
+
|
|
199
203
|
## Development
|
|
200
204
|
|
|
201
205
|
Run tests from the repository root:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|