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.
Files changed (43) hide show
  1. {mathcraft_ocr-0.2.3/mathcraft_ocr.egg-info → mathcraft_ocr-0.2.5}/PKG-INFO +5 -1
  2. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/README.md +20 -21
  3. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/README_MATHCRAFT_OCR.md +4 -0
  4. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/__init__.py +1 -1
  5. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/text_recognizer.py +6 -4
  6. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/formula_lines.py +94 -8
  7. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/manifests/models.v1.json +1 -1
  8. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/providers.py +8 -0
  9. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/runtime.py +20 -4
  10. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/serialization.py +2 -0
  11. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5/mathcraft_ocr.egg-info}/PKG-INFO +5 -1
  12. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/pyproject.toml +1 -1
  13. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/LICENSE +0 -0
  14. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/MANIFEST.in +0 -0
  15. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/__main__.py +0 -0
  16. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/__init__.py +0 -0
  17. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/common.py +0 -0
  18. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/formula_detector.py +0 -0
  19. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/formula_recognizer.py +0 -0
  20. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/adapters/text_detector.py +0 -0
  21. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/api.py +0 -0
  22. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/cache.py +0 -0
  23. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/cli.py +0 -0
  24. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/debug_blocks.py +0 -0
  25. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/doctor.py +0 -0
  26. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/downloader.py +0 -0
  27. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/error_patterns.py +0 -0
  28. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/errors.py +0 -0
  29. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/hardware.py +0 -0
  30. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/image.py +0 -0
  31. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/latex_alignment.py +0 -0
  32. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/latex_quality.py +0 -0
  33. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/layout.py +0 -0
  34. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/manifest.py +0 -0
  35. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/profiles.py +0 -0
  36. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/results.py +0 -0
  37. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr/worker.py +0 -0
  38. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/SOURCES.txt +0 -0
  39. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/dependency_links.txt +0 -0
  40. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/entry_points.txt +0 -0
  41. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/requires.txt +0 -0
  42. {mathcraft_ocr-0.2.3 → mathcraft_ocr-0.2.5}/mathcraft_ocr.egg-info/top_level.txt +0 -0
  43. {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
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.3`.
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.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. The active `v1.0.0` ONNX model asset set is unchanged.
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
- 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.
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 updated the runtime-side formula recognition path with line-aware formula splitting and reassembly. This improved long multi-line formula handling without changing the active `v1.0.0` ONNX model asset set.
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
- ## Benchmark Snapshot
204
+ ## Reproducible Benchmarks
198
205
 
199
- Local `block_layout_regression_v4` telemetry:
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
- | Metric | Value |
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
- Environment:
214
+ ![Published-context UniMER-Test BLEU comparison](benchmarks/figures/unimer_public_bleu_comparison.png)
212
215
 
213
- ```text
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:
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.2.3"
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": {
@@ -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:
@@ -20,7 +20,7 @@
20
20
  "files": [
21
21
  {
22
22
  "path": "config.json",
23
- "sha256": "9f3812441d397c871b9b2a74e8d956b939aec5f4f45745bba9214e968d56449d"
23
+ "sha256": "cc60b5e3fa221c49086147e82583b3bf62a62917bddf38decfccf7ccfdb1e53b"
24
24
  },
25
25
  {
26
26
  "path": "encoder_model.onnx",
@@ -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 = _repair_severe_segmented_lines(
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 _repair_severe_segmented_lines(
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
- if not has_severe_latex_quality_issue(line_text):
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
- if _prefer_formula_fallback(line_text, line_score, fallback_text, fallback_score):
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
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:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mathcraft-ocr"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "ONNX-only OCR runtime for mathematical documents"
9
9
  readme = "README_MATHCRAFT_OCR.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes