mathcraft-ocr 0.2.8__tar.gz → 0.2.9__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.

Potentially problematic release.


This version of mathcraft-ocr might be problematic. Click here for more details.

Files changed (45) hide show
  1. {mathcraft_ocr-0.2.8/mathcraft_ocr.egg-info → mathcraft_ocr-0.2.9}/PKG-INFO +59 -12
  2. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/README_MATHCRAFT_OCR.md +47 -4
  3. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/__init__.py +7 -1
  4. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/adapters/common.py +33 -12
  5. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/adapters/text_recognizer.py +12 -60
  6. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/cli.py +7 -4
  7. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/doctor.py +4 -2
  8. mathcraft_ocr-0.2.9/mathcraft_ocr/providers.py +279 -0
  9. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/runtime.py +5 -1
  10. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/serialization.py +4 -0
  11. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/worker.py +4 -1
  12. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9/mathcraft_ocr.egg-info}/PKG-INFO +59 -12
  13. mathcraft_ocr-0.2.9/mathcraft_ocr.egg-info/requires.txt +33 -0
  14. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/pyproject.toml +15 -8
  15. mathcraft_ocr-0.2.8/mathcraft_ocr/providers.py +0 -128
  16. mathcraft_ocr-0.2.8/mathcraft_ocr.egg-info/requires.txt +0 -30
  17. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/LICENSE +0 -0
  18. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/MANIFEST.in +0 -0
  19. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/__main__.py +0 -0
  20. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/adapters/__init__.py +0 -0
  21. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/adapters/formula_detector.py +0 -0
  22. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/adapters/formula_recognizer.py +0 -0
  23. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/adapters/text_detector.py +0 -0
  24. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/api.py +0 -0
  25. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/cache.py +0 -0
  26. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/debug_blocks.py +0 -0
  27. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/devices.py +0 -0
  28. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/downloader.py +0 -0
  29. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/error_patterns.py +0 -0
  30. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/errors.py +0 -0
  31. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/formula_lines.py +0 -0
  32. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/hardware.py +0 -0
  33. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/image.py +0 -0
  34. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/latex_alignment.py +0 -0
  35. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/latex_quality.py +0 -0
  36. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/layout.py +0 -0
  37. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/manifest.py +0 -0
  38. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/manifests/models.v1.json +0 -0
  39. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/profiles.py +0 -0
  40. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr/results.py +0 -0
  41. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr.egg-info/SOURCES.txt +0 -0
  42. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr.egg-info/dependency_links.txt +0 -0
  43. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr.egg-info/entry_points.txt +0 -0
  44. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/mathcraft_ocr.egg-info/top_level.txt +0 -0
  45. {mathcraft_ocr-0.2.8 → mathcraft_ocr-0.2.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mathcraft-ocr
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: ONNX-only OCR runtime for mathematical documents
5
5
  Author: SakuraMathcraft
6
6
  License-Expression: GPL-3.0-only
@@ -21,21 +21,25 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Topic :: Scientific/Engineering :: Image Recognition
23
23
  Classifier: Topic :: Text Processing
24
- Requires-Python: >=3.10
24
+ Requires-Python: <3.14,>=3.10
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
- Requires-Dist: numpy<3,>=1.26
28
- Requires-Dist: pillow<12,>=10
27
+ Requires-Dist: numpy<3,>=2
28
+ Requires-Dist: pillow<13,>=12
29
29
  Requires-Dist: opencv-python<5,>=4.10
30
30
  Requires-Dist: rapidocr==3.5.0
31
31
  Requires-Dist: transformers==4.55.4
32
32
  Requires-Dist: tokenizers==0.21.4
33
33
  Provides-Extra: cpu
34
- Requires-Dist: onnxruntime<1.26,>=1.19.2; python_version < "3.13" and extra == "cpu"
35
- Requires-Dist: onnxruntime<1.26,>=1.20; python_version >= "3.13" and extra == "cpu"
34
+ Requires-Dist: onnxruntime<1.30,>=1.20; extra == "cpu"
36
35
  Provides-Extra: gpu
37
- Requires-Dist: onnxruntime-gpu<1.26,>=1.19.2; python_version < "3.13" and extra == "gpu"
38
- Requires-Dist: onnxruntime-gpu<1.26,>=1.20; python_version >= "3.13" and extra == "gpu"
36
+ Requires-Dist: onnxruntime-gpu<1.30,>=1.27; extra == "gpu"
37
+ Provides-Extra: gpu-cu12
38
+ Requires-Dist: onnxruntime-gpu<1.27,>=1.21; extra == "gpu-cu12"
39
+ Provides-Extra: directml
40
+ Requires-Dist: onnxruntime-directml<1.30,>=1.20; platform_system == "Windows" and extra == "directml"
41
+ Provides-Extra: openvino
42
+ Requires-Dist: onnxruntime-openvino<1.30,>=1.20; (platform_system == "Windows" or platform_system == "Linux") and extra == "openvino"
39
43
  Provides-Extra: dev
40
44
  Requires-Dist: ruff>=0.8; extra == "dev"
41
45
  Requires-Dist: build>=1.2; extra == "dev"
@@ -76,9 +80,27 @@ GPU backend:
76
80
  pip install "mathcraft-ocr[gpu]"
77
81
  ```
78
82
 
79
- Install only one backend extra in a clean environment. `onnxruntime` and `onnxruntime-gpu` should not be mixed in the same environment.
83
+ CUDA 12 backend:
80
84
 
81
- 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 it keeps a broad stable PyPI range; CUDA 11.x users installing manually should use the CUDA 11 feed shown by the wizard.
85
+ ```powershell
86
+ pip install "mathcraft-ocr[gpu-cu12]"
87
+ ```
88
+
89
+ Windows DirectML backend:
90
+
91
+ ```powershell
92
+ pip install "mathcraft-ocr[directml]"
93
+ ```
94
+
95
+ Intel OpenVINO backend on Windows or Linux:
96
+
97
+ ```powershell
98
+ pip install "mathcraft-ocr[openvino]"
99
+ ```
100
+
101
+ Install only one backend extra in a clean environment. The CPU, CUDA/TensorRT, DirectML, and OpenVINO ONNX Runtime distributions must not be mixed in the same environment. The standard `onnxruntime` macOS wheel includes CoreML EP support.
102
+
103
+ LaTeXSnipper's dependency wizard selects the ONNX Runtime GPU line from the detected CUDA toolkit: CUDA 11.x uses ONNX Runtime 1.20 from the official CUDA 11 feed, CUDA 12.x uses ONNX Runtime 1.21-1.26 from PyPI, and CUDA 13.x uses ONNX Runtime 1.27-1.29 from PyPI. The static `mathcraft-ocr[gpu]` extra follows the current PyPI default (CUDA 13 and Python 3.11+); use `gpu-cu12` for CUDA 12. CUDA 11 installations must also select the official CUDA 11 package feed, so LaTeXSnipper's dependency wizard is recommended for that configuration.
82
104
 
83
105
  ## Quick Start
84
106
 
@@ -190,9 +212,34 @@ Model artifacts are downloaded from the MathCraft-Models release assets declared
190
212
 
191
213
  `provider_preference` accepts:
192
214
 
193
- - `auto`: prefer CUDA when available and valid, otherwise CPU.
215
+ - `auto`: prefer CUDA, then a platform accelerator, and finally CPU; TensorRT remains explicit because of its first-use engine build cost.
194
216
  - `cpu`: force CPU.
195
- - `gpu`: request CUDA-capable ONNX Runtime.
217
+ - `gpu`: request the first available supported accelerator.
218
+ - `cuda`: request CUDA.
219
+ - `tensorrt` or `trt`: request TensorRT, followed by CUDA and CPU for unsupported nodes.
220
+ - `directml` or `dml`: request DirectML.
221
+ - `coreml`: request CoreML.
222
+ - `openvino`: request OpenVINO.
223
+
224
+ Advanced callers can pass an explicit ordered provider list and provider options:
225
+
226
+ ```python
227
+ runtime = MathCraftRuntime(
228
+ providers=[
229
+ (
230
+ "TensorrtExecutionProvider",
231
+ {
232
+ "device_id": 0,
233
+ "trt_engine_cache_enable": True,
234
+ "trt_engine_cache_path": "./trt-cache",
235
+ },
236
+ ),
237
+ ("CUDAExecutionProvider", {"device_id": 0}),
238
+ ]
239
+ )
240
+ ```
241
+
242
+ `auto` deliberately prefers CUDA over TensorRT because TensorRT can spend substantial time building an engine on first use. Select `tensorrt` explicitly when its native runtime is installed; enable its engine cache for repeated startup performance.
196
243
 
197
244
  The actual provider is available on recognition results through the `provider` field. Doctor and warmup reports also expose `device_id`, `device_name`, `device_uuid`, and `device_verified` under `provider_info`. GPU sessions bind the reported `device_id` explicitly; `device_verified` becomes true only after the runtime confirms the device used by initialized inference sessions.
198
245
 
@@ -30,9 +30,27 @@ GPU backend:
30
30
  pip install "mathcraft-ocr[gpu]"
31
31
  ```
32
32
 
33
- Install only one backend extra in a clean environment. `onnxruntime` and `onnxruntime-gpu` should not be mixed in the same environment.
33
+ CUDA 12 backend:
34
34
 
35
- 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 it keeps a broad stable PyPI range; CUDA 11.x users installing manually should use the CUDA 11 feed shown by the wizard.
35
+ ```powershell
36
+ pip install "mathcraft-ocr[gpu-cu12]"
37
+ ```
38
+
39
+ Windows DirectML backend:
40
+
41
+ ```powershell
42
+ pip install "mathcraft-ocr[directml]"
43
+ ```
44
+
45
+ Intel OpenVINO backend on Windows or Linux:
46
+
47
+ ```powershell
48
+ pip install "mathcraft-ocr[openvino]"
49
+ ```
50
+
51
+ Install only one backend extra in a clean environment. The CPU, CUDA/TensorRT, DirectML, and OpenVINO ONNX Runtime distributions must not be mixed in the same environment. The standard `onnxruntime` macOS wheel includes CoreML EP support.
52
+
53
+ LaTeXSnipper's dependency wizard selects the ONNX Runtime GPU line from the detected CUDA toolkit: CUDA 11.x uses ONNX Runtime 1.20 from the official CUDA 11 feed, CUDA 12.x uses ONNX Runtime 1.21-1.26 from PyPI, and CUDA 13.x uses ONNX Runtime 1.27-1.29 from PyPI. The static `mathcraft-ocr[gpu]` extra follows the current PyPI default (CUDA 13 and Python 3.11+); use `gpu-cu12` for CUDA 12. CUDA 11 installations must also select the official CUDA 11 package feed, so LaTeXSnipper's dependency wizard is recommended for that configuration.
36
54
 
37
55
  ## Quick Start
38
56
 
@@ -144,9 +162,34 @@ Model artifacts are downloaded from the MathCraft-Models release assets declared
144
162
 
145
163
  `provider_preference` accepts:
146
164
 
147
- - `auto`: prefer CUDA when available and valid, otherwise CPU.
165
+ - `auto`: prefer CUDA, then a platform accelerator, and finally CPU; TensorRT remains explicit because of its first-use engine build cost.
148
166
  - `cpu`: force CPU.
149
- - `gpu`: request CUDA-capable ONNX Runtime.
167
+ - `gpu`: request the first available supported accelerator.
168
+ - `cuda`: request CUDA.
169
+ - `tensorrt` or `trt`: request TensorRT, followed by CUDA and CPU for unsupported nodes.
170
+ - `directml` or `dml`: request DirectML.
171
+ - `coreml`: request CoreML.
172
+ - `openvino`: request OpenVINO.
173
+
174
+ Advanced callers can pass an explicit ordered provider list and provider options:
175
+
176
+ ```python
177
+ runtime = MathCraftRuntime(
178
+ providers=[
179
+ (
180
+ "TensorrtExecutionProvider",
181
+ {
182
+ "device_id": 0,
183
+ "trt_engine_cache_enable": True,
184
+ "trt_engine_cache_path": "./trt-cache",
185
+ },
186
+ ),
187
+ ("CUDAExecutionProvider", {"device_id": 0}),
188
+ ]
189
+ )
190
+ ```
191
+
192
+ `auto` deliberately prefers CUDA over TensorRT because TensorRT can spend substantial time building an engine on first use. Select `tensorrt` explicitly when its native runtime is installed; enable its engine cache for repeated startup performance.
150
193
 
151
194
  The actual provider is available on recognition results through the `provider` field. Doctor and warmup reports also expose `device_id`, `device_name`, `device_uuid`, and `device_verified` under `provider_info`. GPU sessions bind the reported `device_id` explicitly; `device_verified` becomes true only after the runtime confirms the device used by initialized inference sessions.
152
195
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.2.8"
5
+ __version__ = "0.2.9"
6
6
 
7
7
  __all__ = [
8
8
  "DoctorReport",
@@ -12,6 +12,8 @@ __all__ = [
12
12
  "MathCraftRuntime",
13
13
  "MixedRecognitionResult",
14
14
  "OCRRegion",
15
+ "ProviderInfo",
16
+ "ProviderSpec",
15
17
  "__version__",
16
18
  "run_doctor",
17
19
  ]
@@ -32,6 +34,10 @@ def __getattr__(name: str) -> object:
32
34
  from . import doctor
33
35
 
34
36
  return getattr(doctor, name)
37
+ if name in {"ProviderInfo", "ProviderSpec"}:
38
+ from . import providers
39
+
40
+ return getattr(providers, name)
35
41
  if name == "MathCraftError":
36
42
  from .errors import MathCraftError
37
43
 
@@ -6,7 +6,7 @@ import importlib
6
6
  from functools import lru_cache
7
7
  from pathlib import Path
8
8
 
9
- from ..providers import GPU_PROVIDER_NAMES, ProviderInfo
9
+ from ..providers import ACCELERATOR_PROVIDER_NAMES, ProviderInfo
10
10
 
11
11
 
12
12
  ProviderOptions = tuple[tuple[str, str], ...]
@@ -17,12 +17,14 @@ def _ort():
17
17
  return importlib.import_module("onnxruntime")
18
18
 
19
19
 
20
- def session_providers(provider_info: ProviderInfo) -> list[str | tuple[str, dict[str, int]]]:
20
+ def session_providers(provider_info: ProviderInfo) -> list[str | tuple[str, dict[str, object]]]:
21
+ if provider_info.provider_specs:
22
+ return [spec.as_ort_provider() for spec in provider_info.provider_specs]
21
23
  available = list(provider_info.available_providers)
22
24
  active = provider_info.active_provider
23
- if active and active in GPU_PROVIDER_NAMES and "CPUExecutionProvider" in available:
25
+ if active and active in ACCELERATOR_PROVIDER_NAMES and "CPUExecutionProvider" in available:
24
26
  return [(active, {"device_id": int(provider_info.device_id or 0)}), "CPUExecutionProvider"]
25
- if active and active in GPU_PROVIDER_NAMES:
27
+ if active and active in ACCELERATOR_PROVIDER_NAMES:
26
28
  return [(active, {"device_id": int(provider_info.device_id or 0)})]
27
29
  if "CPUExecutionProvider" in available:
28
30
  return ["CPUExecutionProvider"]
@@ -44,15 +46,34 @@ def _create_session_cached(model_path: str, providers: tuple[ProviderConfig, ...
44
46
  (name, {key: value for key, value in options}) if options else name
45
47
  for name, options in providers
46
48
  ]
47
- return ort.InferenceSession(
48
- model_path,
49
- providers=configured,
50
- enable_fallback=False,
51
- )
49
+ kwargs = {
50
+ "providers": configured,
51
+ "enable_fallback": False,
52
+ }
53
+ session_options = _session_options(ort, providers)
54
+ if session_options is not None:
55
+ kwargs["sess_options"] = session_options
56
+ return ort.InferenceSession(model_path, **kwargs)
57
+
58
+
59
+ def _session_options(ort, providers: tuple[ProviderConfig, ...]):
60
+ create_options = getattr(ort, "SessionOptions", None)
61
+ if not callable(create_options):
62
+ return None
63
+ session_options = create_options()
64
+ session_options.log_severity_level = 4
65
+ names = {name for name, _options in providers}
66
+ if "DmlExecutionProvider" not in names:
67
+ return session_options
68
+ session_options.enable_mem_pattern = False
69
+ execution_mode = getattr(getattr(ort, "ExecutionMode", None), "ORT_SEQUENTIAL", None)
70
+ if execution_mode is not None:
71
+ session_options.execution_mode = execution_mode
72
+ return session_options
52
73
 
53
74
 
54
75
  def _freeze_provider_config(
55
- providers: list[str | tuple[str, dict[str, int]]],
76
+ providers: list[str | tuple[str, dict[str, object]]],
56
77
  ) -> tuple[ProviderConfig, ...]:
57
78
  frozen: list[ProviderConfig] = []
58
79
  for item in providers:
@@ -82,12 +103,12 @@ def validate_session_provider(
82
103
  actual = list(session.get_providers() or [])
83
104
  active = str(active_provider or "")
84
105
  if not actual or actual[0] != active:
85
- provider_kind = "ONNX GPU provider" if active in GPU_PROVIDER_NAMES else "ONNX provider"
106
+ provider_kind = "ONNX accelerator provider" if active in ACCELERATOR_PROVIDER_NAMES else "ONNX provider"
86
107
  raise RuntimeError(
87
108
  f"requested {provider_kind} {active or '<none>'}, "
88
109
  f"but {runtime_name} session providers are {actual}"
89
110
  )
90
- if active in GPU_PROVIDER_NAMES:
111
+ if active in {"CUDAExecutionProvider", "TensorrtExecutionProvider", "DmlExecutionProvider"}:
91
112
  get_options = getattr(session, "get_provider_options", None)
92
113
  if callable(get_options):
93
114
  options_by_provider = get_options() or {}
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  from functools import lru_cache
6
7
  from pathlib import Path
7
8
 
@@ -10,7 +11,11 @@ from rapidocr import EngineType, LangRec, ModelType, OCRVersion
10
11
  from rapidocr.ch_ppocr_rec import TextRecInput, TextRecognizer
11
12
  from rapidocr.utils.typings import TaskType
12
13
 
13
- from .common import validate_session_provider
14
+ from .common import create_session
15
+ from ..providers import ProviderInfo
16
+
17
+
18
+ logging.getLogger("RapidOCR").setLevel(logging.WARNING)
14
19
 
15
20
 
16
21
  class _Config(dict):
@@ -56,26 +61,13 @@ def recognize_pp_text_lines(
56
61
 
57
62
  def _create_pp_text_recognizer(model_dir: Path, provider_info) -> TextRecognizer:
58
63
  model_dir = model_dir.resolve()
59
- active_provider = str(getattr(provider_info, "active_provider", "") or "")
60
- device_id = int(getattr(provider_info, "device_id", 0) or 0)
61
- use_cuda = active_provider in {"CUDAExecutionProvider", "TensorrtExecutionProvider"}
62
- use_dml = active_provider == "DmlExecutionProvider"
63
- return _create_pp_text_recognizer_cached(
64
- str(model_dir),
65
- active_provider,
66
- device_id,
67
- use_cuda,
68
- use_dml,
69
- )
64
+ return _create_pp_text_recognizer_cached(str(model_dir), provider_info)
70
65
 
71
66
 
72
67
  @lru_cache(maxsize=8)
73
68
  def _create_pp_text_recognizer_cached(
74
69
  model_dir: str,
75
- active_provider: str,
76
- device_id: int,
77
- use_cuda: bool,
78
- use_dml: bool,
70
+ provider_info: ProviderInfo,
79
71
  ) -> TextRecognizer:
80
72
  model_dir = Path(model_dir)
81
73
  model_candidates = sorted(model_dir.glob("**/*rec*.onnx"))
@@ -89,6 +81,7 @@ def _create_pp_text_recognizer_cached(
89
81
  is_server = "server" in model_name or "server" in model_dir.name
90
82
  is_v5 = "v5" in model_name or "v5" in model_dir.name
91
83
  is_english = dict_path.name == "en_dict.txt"
84
+ session = create_session(model_path, provider_info)
92
85
  config = _Config({
93
86
  "engine_type": EngineType.ONNXRUNTIME,
94
87
  "lang_type": LangRec.EN if is_english else LangRec.CH,
@@ -101,51 +94,10 @@ def _create_pp_text_recognizer_cached(
101
94
  "rec_img_shape": [3, 48, 320],
102
95
  "rec_batch_num": 6,
103
96
  "font_path": None,
104
- "engine_cfg": {
105
- "intra_op_num_threads": -1,
106
- "inter_op_num_threads": -1,
107
- "enable_cpu_mem_arena": False,
108
- "cpu_ep_cfg": {"arena_extend_strategy": "kSameAsRequested"},
109
- "use_cuda": use_cuda,
110
- "cuda_ep_cfg": {
111
- "device_id": device_id,
112
- "arena_extend_strategy": "kNextPowerOfTwo",
113
- "cudnn_conv_algo_search": "EXHAUSTIVE",
114
- "do_copy_in_default_stream": True,
115
- },
116
- "use_dml": use_dml,
117
- "dm_ep_cfg": {"device_id": device_id},
118
- "use_cann": False,
119
- "cann_ep_cfg": {
120
- "device_id": 0,
121
- "arena_extend_strategy": "kNextPowerOfTwo",
122
- "npu_mem_limit": 21474836480,
123
- "op_select_impl_mode": "high_performance",
124
- "optypelist_for_implmode": "Gelu",
125
- "enable_cann_graph": True,
126
- },
127
- },
97
+ "session": session,
98
+ "engine_cfg": {},
128
99
  })
129
- recognizer = TextRecognizer(config)
130
- _enforce_strict_provider(recognizer, active_provider, device_id)
131
- return recognizer
132
-
133
-
134
- def _enforce_strict_provider(
135
- recognizer: TextRecognizer,
136
- active_provider: str,
137
- device_id: int = 0,
138
- ) -> None:
139
- engine = getattr(recognizer, "session", None)
140
- session = getattr(engine, "session", None)
141
- if session is None:
142
- raise RuntimeError("RapidOCR did not expose its ONNX Runtime session")
143
- validate_session_provider(
144
- session,
145
- active_provider,
146
- device_id,
147
- runtime_name="RapidOCR",
148
- )
100
+ return TextRecognizer(config)
149
101
 
150
102
 
151
103
  def clear_text_recognizer_cache() -> None:
@@ -16,6 +16,9 @@ from .serialization import (
16
16
  )
17
17
 
18
18
 
19
+ _PROVIDER_HELP = "auto, cpu, gpu, cuda, tensorrt, directml, coreml, or openvino"
20
+
21
+
19
22
  def _resolve_ocr_output_path(args: argparse.Namespace) -> Path | None:
20
23
  output = str(getattr(args, "output", "") or "").strip()
21
24
  output_dir = str(getattr(args, "output_dir", "") or "").strip()
@@ -45,22 +48,22 @@ def build_parser() -> argparse.ArgumentParser:
45
48
  models_sub.add_parser("check")
46
49
 
47
50
  doctor = sub.add_parser("doctor")
48
- doctor.add_argument("--provider", default="auto")
51
+ doctor.add_argument("--provider", default="auto", help=_PROVIDER_HELP)
49
52
 
50
53
  warmup = sub.add_parser("warmup")
51
54
  warmup.add_argument("--profile", default="formula")
52
- warmup.add_argument("--provider", default="auto")
55
+ warmup.add_argument("--provider", default="auto", help=_PROVIDER_HELP)
53
56
 
54
57
  ocr = sub.add_parser("ocr")
55
58
  ocr.add_argument("image")
56
59
  ocr.add_argument("--profile", choices=("formula", "text", "mixed"), default="mixed")
57
- ocr.add_argument("--provider", default="auto")
60
+ ocr.add_argument("--provider", default="auto", help=_PROVIDER_HELP)
58
61
  ocr.add_argument("--output", "-o", default="")
59
62
  ocr.add_argument("--output-dir", default="")
60
63
  ocr.add_argument("--json", action="store_true", dest="as_json")
61
64
 
62
65
  worker = sub.add_parser("worker")
63
- worker.add_argument("--provider", default="auto")
66
+ worker.add_argument("--provider", default="auto", help=_PROVIDER_HELP)
64
67
  return parser
65
68
 
66
69
 
@@ -3,12 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import sys
6
+ from collections.abc import Sequence
6
7
  from dataclasses import dataclass
7
8
  from pathlib import Path
8
9
 
9
10
  from .cache import inspect_manifest_roots, resolve_model_roots, resolve_user_models_dir
10
11
  from .manifest import Manifest, load_manifest
11
- from .providers import ProviderInfo, detect_providers
12
+ from .providers import ProviderInfo, ProviderRequest, detect_providers
12
13
 
13
14
 
14
15
  @dataclass(frozen=True)
@@ -28,6 +29,7 @@ def run_doctor(
28
29
  bundled_models_dir: str | Path | None = None,
29
30
  manifest: Manifest | None = None,
30
31
  provider_preference: str = "auto",
32
+ providers: Sequence[ProviderRequest] | None = None,
31
33
  include_optional: bool = True,
32
34
  ) -> DoctorReport:
33
35
  manifest_obj = manifest or load_manifest()
@@ -36,7 +38,7 @@ def run_doctor(
36
38
  states = inspect_manifest_roots(
37
39
  model_roots, manifest_obj, include_optional=include_optional
38
40
  )
39
- provider_info = detect_providers(prefer=provider_preference)
41
+ provider_info = detect_providers(prefer=provider_preference, providers=providers)
40
42
  return DoctorReport(
41
43
  python_executable=sys.executable,
42
44
  cache_dir=cache_root,
@@ -0,0 +1,279 @@
1
+ # coding: utf-8
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from collections.abc import Mapping, Sequence
7
+ from dataclasses import dataclass, replace
8
+
9
+ from .devices import confirm_device_identity, resolve_device_identity
10
+ from .errors import ProviderError
11
+
12
+
13
+ MAIN_PROVIDER_NAMES = (
14
+ "CPUExecutionProvider",
15
+ "CUDAExecutionProvider",
16
+ "TensorrtExecutionProvider",
17
+ "DmlExecutionProvider",
18
+ "CoreMLExecutionProvider",
19
+ "OpenVINOExecutionProvider",
20
+ )
21
+
22
+ ACCELERATOR_PROVIDER_NAMES = tuple(
23
+ name for name in MAIN_PROVIDER_NAMES if name != "CPUExecutionProvider"
24
+ )
25
+
26
+ # Kept as a compatibility alias for integrations importing the old name.
27
+ GPU_PROVIDER_NAMES = ACCELERATOR_PROVIDER_NAMES
28
+
29
+ _PROVIDER_ALIASES = {
30
+ "cpu": "CPUExecutionProvider",
31
+ "cuda": "CUDAExecutionProvider",
32
+ "tensorrt": "TensorrtExecutionProvider",
33
+ "trt": "TensorrtExecutionProvider",
34
+ "directml": "DmlExecutionProvider",
35
+ "dml": "DmlExecutionProvider",
36
+ "coreml": "CoreMLExecutionProvider",
37
+ "openvino": "OpenVINOExecutionProvider",
38
+ }
39
+
40
+ _AUTO_PRIORITY = (
41
+ "CUDAExecutionProvider",
42
+ "DmlExecutionProvider",
43
+ "CoreMLExecutionProvider",
44
+ "OpenVINOExecutionProvider",
45
+ "CPUExecutionProvider",
46
+ )
47
+
48
+ _ACCELERATOR_PRIORITY = (
49
+ "CUDAExecutionProvider",
50
+ "TensorrtExecutionProvider",
51
+ "DmlExecutionProvider",
52
+ "CoreMLExecutionProvider",
53
+ "OpenVINOExecutionProvider",
54
+ )
55
+
56
+ ProviderRequest = str | tuple[str, Mapping[str, object]]
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class ProviderSpec:
61
+ name: str
62
+ options: tuple[tuple[str, str], ...] = ()
63
+
64
+ def as_ort_provider(self) -> str | tuple[str, dict[str, str]]:
65
+ if not self.options:
66
+ return self.name
67
+ return self.name, dict(self.options)
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class ProviderInfo:
72
+ available_providers: tuple[str, ...]
73
+ active_provider: str | None
74
+ device: str
75
+ gpu_requested: bool
76
+ gpu_runtime_ok: bool
77
+ device_id: int | None = None
78
+ device_name: str = ""
79
+ device_uuid: str = ""
80
+ device_verified: bool = False
81
+ provider_specs: tuple[ProviderSpec, ...] = ()
82
+
83
+ @property
84
+ def use_cuda(self) -> bool:
85
+ return self.active_provider in {"CUDAExecutionProvider", "TensorrtExecutionProvider"}
86
+
87
+ @property
88
+ def use_dml(self) -> bool:
89
+ return self.active_provider == "DmlExecutionProvider"
90
+
91
+ @property
92
+ def use_tensorrt(self) -> bool:
93
+ return self.active_provider == "TensorrtExecutionProvider"
94
+
95
+ @property
96
+ def use_coreml(self) -> bool:
97
+ return self.active_provider == "CoreMLExecutionProvider"
98
+
99
+ @property
100
+ def use_openvino(self) -> bool:
101
+ return self.active_provider == "OpenVINOExecutionProvider"
102
+
103
+ @property
104
+ def requested_providers(self) -> tuple[str, ...]:
105
+ if self.provider_specs:
106
+ return tuple(spec.name for spec in self.provider_specs)
107
+ return (self.active_provider,) if self.active_provider else ()
108
+
109
+
110
+ def detect_providers(
111
+ prefer: str = "auto",
112
+ providers: Sequence[ProviderRequest] | None = None,
113
+ ) -> ProviderInfo:
114
+ prefer_norm = (prefer or "auto").strip().lower()
115
+ if providers is None and prefer_norm not in {"auto", "cpu", "gpu", *_PROVIDER_ALIASES}:
116
+ raise ProviderError(f"unsupported provider preference: {prefer}")
117
+ try:
118
+ ort = importlib.import_module("onnxruntime")
119
+ except Exception as exc: # pragma: no cover - import environment dependent
120
+ raise ProviderError(f"failed to import onnxruntime: {exc}") from exc
121
+
122
+ get_available_providers = getattr(ort, "get_available_providers", None)
123
+ if not callable(get_available_providers):
124
+ origin = getattr(ort, "__file__", None) or "<namespace package>"
125
+ raise ProviderError(
126
+ "onnxruntime dependency is incomplete: missing get_available_providers "
127
+ f"(origin={origin})"
128
+ )
129
+
130
+ try:
131
+ available = tuple(get_available_providers())
132
+ except Exception as exc: # pragma: no cover - runtime environment dependent
133
+ raise ProviderError(f"failed to query ONNX providers: {exc}") from exc
134
+
135
+ specs = _resolve_provider_specs(prefer_norm, providers, available)
136
+ active = specs[0].name
137
+ accelerated = active in ACCELERATOR_PROVIDER_NAMES
138
+ device = (
139
+ "gpu"
140
+ if active in {"CUDAExecutionProvider", "TensorrtExecutionProvider", "DmlExecutionProvider"}
141
+ else "accelerator"
142
+ if accelerated
143
+ else "cpu"
144
+ )
145
+ return _provider_info(
146
+ available_providers=available,
147
+ active_provider=active,
148
+ device=device,
149
+ gpu_requested=accelerated,
150
+ gpu_runtime_ok=accelerated,
151
+ provider_specs=specs,
152
+ )
153
+
154
+
155
+ def _resolve_provider_specs(
156
+ prefer: str,
157
+ providers: Sequence[ProviderRequest] | None,
158
+ available: tuple[str, ...],
159
+ ) -> tuple[ProviderSpec, ...]:
160
+ if providers is not None:
161
+ if isinstance(providers, (str, bytes)) or not providers:
162
+ raise ProviderError("providers must be a non-empty sequence")
163
+ specs = tuple(_normalize_provider_request(item) for item in providers)
164
+ _ensure_available(specs, available)
165
+ return specs
166
+
167
+ if prefer == "auto":
168
+ active = next((name for name in _AUTO_PRIORITY if name in available), None)
169
+ if active is None:
170
+ raise ProviderError(f"no supported ONNX execution provider is available: {available}")
171
+ return _default_provider_chain(active, available)
172
+
173
+ if prefer == "gpu":
174
+ active = next((name for name in _ACCELERATOR_PRIORITY if name in available), None)
175
+ if active is None:
176
+ raise ProviderError(f"GPU provider was requested but none is available: {available}")
177
+ return _default_provider_chain(active, available)
178
+
179
+ active = _PROVIDER_ALIASES[prefer]
180
+ if active not in available:
181
+ raise ProviderError(f"requested ONNX provider {active} is unavailable: {available}")
182
+ return _default_provider_chain(active, available)
183
+
184
+
185
+ def _normalize_provider_request(request: ProviderRequest) -> ProviderSpec:
186
+ if isinstance(request, str):
187
+ raw_name = request
188
+ options: Mapping[str, object] = {}
189
+ elif isinstance(request, tuple) and len(request) == 2 and isinstance(request[1], Mapping):
190
+ raw_name, options = request
191
+ else:
192
+ raise ProviderError(f"invalid provider specification: {request!r}")
193
+ name = _canonical_provider_name(str(raw_name))
194
+ if name not in MAIN_PROVIDER_NAMES:
195
+ raise ProviderError(f"unsupported ONNX provider: {raw_name}")
196
+ frozen_options = tuple(sorted((str(key), str(value)) for key, value in options.items()))
197
+ return ProviderSpec(name=name, options=frozen_options)
198
+
199
+
200
+ def _canonical_provider_name(name: str) -> str:
201
+ stripped = name.strip()
202
+ return _PROVIDER_ALIASES.get(stripped.lower(), stripped)
203
+
204
+
205
+ def _ensure_available(specs: tuple[ProviderSpec, ...], available: tuple[str, ...]) -> None:
206
+ unavailable = tuple(spec.name for spec in specs if spec.name not in available)
207
+ if unavailable:
208
+ raise ProviderError(
209
+ f"requested ONNX providers are unavailable: {unavailable}; available={available}"
210
+ )
211
+
212
+
213
+ def _default_provider_chain(active: str, available: tuple[str, ...]) -> tuple[ProviderSpec, ...]:
214
+ names = [active]
215
+ if active == "TensorrtExecutionProvider" and "CUDAExecutionProvider" in available:
216
+ names.append("CUDAExecutionProvider")
217
+ if active != "CPUExecutionProvider" and "CPUExecutionProvider" in available:
218
+ names.append("CPUExecutionProvider")
219
+ return tuple(ProviderSpec(name=name, options=_default_options(name)) for name in names)
220
+
221
+
222
+ def _default_options(name: str) -> tuple[tuple[str, str], ...]:
223
+ if name in {"CUDAExecutionProvider", "TensorrtExecutionProvider", "DmlExecutionProvider"}:
224
+ return (("device_id", "0"),)
225
+ return ()
226
+
227
+
228
+ def _provider_info(
229
+ *,
230
+ available_providers: tuple[str, ...],
231
+ active_provider: str,
232
+ device: str,
233
+ gpu_requested: bool,
234
+ gpu_runtime_ok: bool,
235
+ provider_specs: tuple[ProviderSpec, ...] = (),
236
+ ) -> ProviderInfo:
237
+ active_spec = next((spec for spec in provider_specs if spec.name == active_provider), None)
238
+ active_options = dict(active_spec.options) if active_spec else {}
239
+ device_id = (
240
+ int(active_options.get("device_id", 0))
241
+ if active_provider in {"CUDAExecutionProvider", "TensorrtExecutionProvider", "DmlExecutionProvider"}
242
+ else None
243
+ )
244
+ identity = resolve_device_identity(active_provider, device_id)
245
+ return ProviderInfo(
246
+ available_providers=available_providers,
247
+ active_provider=active_provider,
248
+ device=device,
249
+ gpu_requested=gpu_requested,
250
+ gpu_runtime_ok=gpu_runtime_ok,
251
+ device_id=identity.device_id,
252
+ device_name=identity.name,
253
+ device_uuid=identity.uuid,
254
+ device_verified=identity.verified,
255
+ provider_specs=provider_specs,
256
+ )
257
+
258
+
259
+ def confirm_provider_device(provider_info: ProviderInfo) -> ProviderInfo:
260
+ identity = confirm_device_identity(provider_info.active_provider, provider_info.device_id)
261
+ return replace(
262
+ provider_info,
263
+ device_id=identity.device_id,
264
+ device_name=identity.name,
265
+ device_uuid=identity.uuid,
266
+ device_verified=identity.verified,
267
+ )
268
+
269
+
270
+ __all__ = [
271
+ "ACCELERATOR_PROVIDER_NAMES",
272
+ "GPU_PROVIDER_NAMES",
273
+ "MAIN_PROVIDER_NAMES",
274
+ "ProviderInfo",
275
+ "ProviderRequest",
276
+ "ProviderSpec",
277
+ "confirm_provider_device",
278
+ "detect_providers",
279
+ ]
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Sequence
5
6
  from dataclasses import dataclass
6
7
  from pathlib import Path
7
8
  import sys
@@ -56,7 +57,7 @@ from .profiles import (
56
57
  TEXT_DETECTOR_ID,
57
58
  TEXT_RECOGNIZER_ID,
58
59
  )
59
- from .providers import ProviderInfo, confirm_provider_device
60
+ from .providers import ProviderInfo, ProviderRequest, confirm_provider_device
60
61
  from .results import Box4P, FormulaRecognitionResult, MathCraftBlock, MixedRecognitionResult, OCRRegion
61
62
 
62
63
 
@@ -98,6 +99,7 @@ class MathCraftRuntime:
98
99
  *,
99
100
  cache_dir: str | Path | None = None,
100
101
  provider_preference: str = "auto",
102
+ providers: Sequence[ProviderRequest] | None = None,
101
103
  manifest: Manifest | None = None,
102
104
  bundled_models_dir: str | Path | None = None,
103
105
  auto_download: bool = True,
@@ -106,6 +108,7 @@ class MathCraftRuntime:
106
108
  self.bundled_models_dir = Path(bundled_models_dir) if bundled_models_dir else None
107
109
  self.model_roots = resolve_model_roots(cache_dir, bundled_models_dir)
108
110
  self.provider_preference = provider_preference
111
+ self.providers = tuple(providers) if providers is not None else None
109
112
  self.manifest = manifest or load_manifest()
110
113
  self.auto_download = auto_download
111
114
  self._warmup_cache: dict[str, WarmupPlan] = {}
@@ -131,6 +134,7 @@ class MathCraftRuntime:
131
134
  bundled_models_dir=self.bundled_models_dir,
132
135
  manifest=self.manifest,
133
136
  provider_preference=self.provider_preference,
137
+ providers=self.providers,
134
138
  )
135
139
 
136
140
  def doctor(self) -> DoctorReport:
@@ -67,6 +67,7 @@ def mixed_result_to_json(result: MixedRecognitionResult) -> dict:
67
67
  def provider_info_to_json(provider_info) -> dict:
68
68
  return {
69
69
  "available_providers": list(provider_info.available_providers),
70
+ "requested_providers": list(provider_info.requested_providers),
70
71
  "active_provider": provider_info.active_provider,
71
72
  "device": provider_info.device,
72
73
  "device_id": provider_info.device_id,
@@ -77,6 +78,9 @@ def provider_info_to_json(provider_info) -> dict:
77
78
  "gpu_runtime_ok": provider_info.gpu_runtime_ok,
78
79
  "use_cuda": provider_info.use_cuda,
79
80
  "use_dml": provider_info.use_dml,
81
+ "use_tensorrt": provider_info.use_tensorrt,
82
+ "use_coreml": provider_info.use_coreml,
83
+ "use_openvino": provider_info.use_openvino,
80
84
  }
81
85
 
82
86
 
@@ -12,6 +12,9 @@ from .runtime import FORMULA_MAX_NEW_TOKENS, MathCraftRuntime
12
12
  from .serialization import doctor_report_to_json, formula_result_to_json, mixed_result_to_json, warmup_plan_to_json
13
13
 
14
14
 
15
+ _PROVIDER_HELP = "auto, cpu, gpu, cuda, tensorrt, directml, coreml, or openvino"
16
+
17
+
15
18
  class MathCraftWorker:
16
19
  def __init__(
17
20
  self,
@@ -113,7 +116,7 @@ def serve_jsonl(
113
116
 
114
117
  def build_parser() -> argparse.ArgumentParser:
115
118
  parser = argparse.ArgumentParser(prog="mathcraft-worker")
116
- parser.add_argument("--provider", default="auto")
119
+ parser.add_argument("--provider", default="auto", help=_PROVIDER_HELP)
117
120
  return parser
118
121
 
119
122
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mathcraft-ocr
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: ONNX-only OCR runtime for mathematical documents
5
5
  Author: SakuraMathcraft
6
6
  License-Expression: GPL-3.0-only
@@ -21,21 +21,25 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Topic :: Scientific/Engineering :: Image Recognition
23
23
  Classifier: Topic :: Text Processing
24
- Requires-Python: >=3.10
24
+ Requires-Python: <3.14,>=3.10
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
- Requires-Dist: numpy<3,>=1.26
28
- Requires-Dist: pillow<12,>=10
27
+ Requires-Dist: numpy<3,>=2
28
+ Requires-Dist: pillow<13,>=12
29
29
  Requires-Dist: opencv-python<5,>=4.10
30
30
  Requires-Dist: rapidocr==3.5.0
31
31
  Requires-Dist: transformers==4.55.4
32
32
  Requires-Dist: tokenizers==0.21.4
33
33
  Provides-Extra: cpu
34
- Requires-Dist: onnxruntime<1.26,>=1.19.2; python_version < "3.13" and extra == "cpu"
35
- Requires-Dist: onnxruntime<1.26,>=1.20; python_version >= "3.13" and extra == "cpu"
34
+ Requires-Dist: onnxruntime<1.30,>=1.20; extra == "cpu"
36
35
  Provides-Extra: gpu
37
- Requires-Dist: onnxruntime-gpu<1.26,>=1.19.2; python_version < "3.13" and extra == "gpu"
38
- Requires-Dist: onnxruntime-gpu<1.26,>=1.20; python_version >= "3.13" and extra == "gpu"
36
+ Requires-Dist: onnxruntime-gpu<1.30,>=1.27; extra == "gpu"
37
+ Provides-Extra: gpu-cu12
38
+ Requires-Dist: onnxruntime-gpu<1.27,>=1.21; extra == "gpu-cu12"
39
+ Provides-Extra: directml
40
+ Requires-Dist: onnxruntime-directml<1.30,>=1.20; platform_system == "Windows" and extra == "directml"
41
+ Provides-Extra: openvino
42
+ Requires-Dist: onnxruntime-openvino<1.30,>=1.20; (platform_system == "Windows" or platform_system == "Linux") and extra == "openvino"
39
43
  Provides-Extra: dev
40
44
  Requires-Dist: ruff>=0.8; extra == "dev"
41
45
  Requires-Dist: build>=1.2; extra == "dev"
@@ -76,9 +80,27 @@ GPU backend:
76
80
  pip install "mathcraft-ocr[gpu]"
77
81
  ```
78
82
 
79
- Install only one backend extra in a clean environment. `onnxruntime` and `onnxruntime-gpu` should not be mixed in the same environment.
83
+ CUDA 12 backend:
80
84
 
81
- 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 it keeps a broad stable PyPI range; CUDA 11.x users installing manually should use the CUDA 11 feed shown by the wizard.
85
+ ```powershell
86
+ pip install "mathcraft-ocr[gpu-cu12]"
87
+ ```
88
+
89
+ Windows DirectML backend:
90
+
91
+ ```powershell
92
+ pip install "mathcraft-ocr[directml]"
93
+ ```
94
+
95
+ Intel OpenVINO backend on Windows or Linux:
96
+
97
+ ```powershell
98
+ pip install "mathcraft-ocr[openvino]"
99
+ ```
100
+
101
+ Install only one backend extra in a clean environment. The CPU, CUDA/TensorRT, DirectML, and OpenVINO ONNX Runtime distributions must not be mixed in the same environment. The standard `onnxruntime` macOS wheel includes CoreML EP support.
102
+
103
+ LaTeXSnipper's dependency wizard selects the ONNX Runtime GPU line from the detected CUDA toolkit: CUDA 11.x uses ONNX Runtime 1.20 from the official CUDA 11 feed, CUDA 12.x uses ONNX Runtime 1.21-1.26 from PyPI, and CUDA 13.x uses ONNX Runtime 1.27-1.29 from PyPI. The static `mathcraft-ocr[gpu]` extra follows the current PyPI default (CUDA 13 and Python 3.11+); use `gpu-cu12` for CUDA 12. CUDA 11 installations must also select the official CUDA 11 package feed, so LaTeXSnipper's dependency wizard is recommended for that configuration.
82
104
 
83
105
  ## Quick Start
84
106
 
@@ -190,9 +212,34 @@ Model artifacts are downloaded from the MathCraft-Models release assets declared
190
212
 
191
213
  `provider_preference` accepts:
192
214
 
193
- - `auto`: prefer CUDA when available and valid, otherwise CPU.
215
+ - `auto`: prefer CUDA, then a platform accelerator, and finally CPU; TensorRT remains explicit because of its first-use engine build cost.
194
216
  - `cpu`: force CPU.
195
- - `gpu`: request CUDA-capable ONNX Runtime.
217
+ - `gpu`: request the first available supported accelerator.
218
+ - `cuda`: request CUDA.
219
+ - `tensorrt` or `trt`: request TensorRT, followed by CUDA and CPU for unsupported nodes.
220
+ - `directml` or `dml`: request DirectML.
221
+ - `coreml`: request CoreML.
222
+ - `openvino`: request OpenVINO.
223
+
224
+ Advanced callers can pass an explicit ordered provider list and provider options:
225
+
226
+ ```python
227
+ runtime = MathCraftRuntime(
228
+ providers=[
229
+ (
230
+ "TensorrtExecutionProvider",
231
+ {
232
+ "device_id": 0,
233
+ "trt_engine_cache_enable": True,
234
+ "trt_engine_cache_path": "./trt-cache",
235
+ },
236
+ ),
237
+ ("CUDAExecutionProvider", {"device_id": 0}),
238
+ ]
239
+ )
240
+ ```
241
+
242
+ `auto` deliberately prefers CUDA over TensorRT because TensorRT can spend substantial time building an engine on first use. Select `tensorrt` explicitly when its native runtime is installed; enable its engine cache for repeated startup performance.
196
243
 
197
244
  The actual provider is available on recognition results through the `provider` field. Doctor and warmup reports also expose `device_id`, `device_name`, `device_uuid`, and `device_verified` under `provider_info`. GPU sessions bind the reported `device_id` explicitly; `device_verified` becomes true only after the runtime confirms the device used by initialized inference sessions.
198
245
 
@@ -0,0 +1,33 @@
1
+ numpy<3,>=2
2
+ pillow<13,>=12
3
+ opencv-python<5,>=4.10
4
+ rapidocr==3.5.0
5
+ transformers==4.55.4
6
+ tokenizers==0.21.4
7
+
8
+ [cpu]
9
+ onnxruntime<1.30,>=1.20
10
+
11
+ [dev]
12
+ ruff>=0.8
13
+ build>=1.2
14
+ twine>=5.0
15
+
16
+ [directml]
17
+
18
+ [directml:platform_system == "Windows"]
19
+ onnxruntime-directml<1.30,>=1.20
20
+
21
+ [gpu]
22
+ onnxruntime-gpu<1.30,>=1.27
23
+
24
+ [gpu-cu12]
25
+ onnxruntime-gpu<1.27,>=1.21
26
+
27
+ [openvino]
28
+
29
+ [openvino:platform_system == "Windows" or platform_system == "Linux"]
30
+ onnxruntime-openvino<1.30,>=1.20
31
+
32
+ [pandoc]
33
+ pypandoc>=1.15
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mathcraft-ocr"
7
- version = "0.2.8"
7
+ version = "0.2.9"
8
8
  description = "ONNX-only OCR runtime for mathematical documents"
9
9
  readme = "README_MATHCRAFT_OCR.md"
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.10,<3.14"
11
11
  license = "GPL-3.0-only"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [
@@ -37,8 +37,8 @@ classifiers = [
37
37
  "Topic :: Text Processing"
38
38
  ]
39
39
  dependencies = [
40
- "numpy>=1.26,<3",
41
- "pillow>=10,<12",
40
+ "numpy>=2,<3",
41
+ "pillow>=12,<13",
42
42
  "opencv-python>=4.10,<5",
43
43
  "rapidocr==3.5.0",
44
44
  "transformers==4.55.4",
@@ -47,12 +47,19 @@ dependencies = [
47
47
 
48
48
  [project.optional-dependencies]
49
49
  cpu = [
50
- "onnxruntime>=1.19.2,<1.26; python_version < '3.13'",
51
- "onnxruntime>=1.20,<1.26; python_version >= '3.13'",
50
+ "onnxruntime>=1.20,<1.30",
52
51
  ]
53
52
  gpu = [
54
- "onnxruntime-gpu>=1.19.2,<1.26; python_version < '3.13'",
55
- "onnxruntime-gpu>=1.20,<1.26; python_version >= '3.13'",
53
+ "onnxruntime-gpu>=1.27,<1.30",
54
+ ]
55
+ gpu-cu12 = [
56
+ "onnxruntime-gpu>=1.21,<1.27",
57
+ ]
58
+ directml = [
59
+ "onnxruntime-directml>=1.20,<1.30; platform_system == 'Windows'",
60
+ ]
61
+ openvino = [
62
+ "onnxruntime-openvino>=1.20,<1.30; platform_system == 'Windows' or platform_system == 'Linux'",
56
63
  ]
57
64
  dev = [
58
65
  "ruff>=0.8",
@@ -1,128 +0,0 @@
1
- # coding: utf-8
2
-
3
- from __future__ import annotations
4
-
5
- import importlib
6
- from dataclasses import dataclass, replace
7
-
8
- from .devices import confirm_device_identity, resolve_device_identity
9
- from .errors import ProviderError
10
-
11
-
12
- GPU_PROVIDER_NAMES = (
13
- "CUDAExecutionProvider",
14
- "TensorrtExecutionProvider",
15
- "DmlExecutionProvider",
16
- )
17
-
18
-
19
- @dataclass(frozen=True)
20
- class ProviderInfo:
21
- available_providers: tuple[str, ...]
22
- active_provider: str | None
23
- device: str
24
- gpu_requested: bool
25
- gpu_runtime_ok: bool
26
- device_id: int | None = None
27
- device_name: str = ""
28
- device_uuid: str = ""
29
- device_verified: bool = False
30
-
31
- @property
32
- def use_cuda(self) -> bool:
33
- return self.active_provider in {"CUDAExecutionProvider", "TensorrtExecutionProvider"}
34
-
35
- @property
36
- def use_dml(self) -> bool:
37
- return self.active_provider == "DmlExecutionProvider"
38
-
39
-
40
- def detect_providers(prefer: str = "auto") -> ProviderInfo:
41
- prefer_norm = (prefer or "auto").strip().lower()
42
- if prefer_norm not in {"auto", "cpu", "gpu"}:
43
- raise ProviderError(f"unsupported provider preference: {prefer}")
44
- try:
45
- ort = importlib.import_module("onnxruntime")
46
- except Exception as exc: # pragma: no cover - import environment dependent
47
- raise ProviderError(f"failed to import onnxruntime: {exc}") from exc
48
-
49
- get_available_providers = getattr(ort, "get_available_providers", None)
50
- if not callable(get_available_providers):
51
- origin = getattr(ort, "__file__", None) or "<namespace package>"
52
- raise ProviderError(
53
- "onnxruntime dependency is incomplete: missing get_available_providers "
54
- f"(origin={origin})"
55
- )
56
-
57
- try:
58
- available = tuple(get_available_providers())
59
- except Exception as exc: # pragma: no cover - runtime environment dependent
60
- raise ProviderError(f"failed to query ONNX providers: {exc}") from exc
61
-
62
- gpu_candidates = tuple(name for name in GPU_PROVIDER_NAMES if name in available)
63
- gpu_visible = bool(gpu_candidates)
64
- if prefer_norm == "cpu":
65
- if "CPUExecutionProvider" not in available:
66
- raise ProviderError(f"CPUExecutionProvider unavailable: {available}")
67
- return _provider_info(
68
- available_providers=available,
69
- active_provider="CPUExecutionProvider",
70
- device="cpu",
71
- gpu_requested=False,
72
- gpu_runtime_ok=False,
73
- )
74
- if gpu_visible:
75
- return _provider_info(
76
- available_providers=available,
77
- active_provider=gpu_candidates[0],
78
- device="gpu",
79
- gpu_requested=True,
80
- gpu_runtime_ok=True,
81
- )
82
- if prefer_norm == "gpu":
83
- raise ProviderError(
84
- f"GPU provider was requested but none is available: {available}"
85
- )
86
- if "CPUExecutionProvider" not in available:
87
- raise ProviderError(f"no supported ONNX execution provider is available: {available}")
88
- return _provider_info(
89
- available_providers=available,
90
- active_provider="CPUExecutionProvider",
91
- device="cpu",
92
- gpu_requested=False,
93
- gpu_runtime_ok=False,
94
- )
95
-
96
-
97
- def _provider_info(
98
- *,
99
- available_providers: tuple[str, ...],
100
- active_provider: str,
101
- device: str,
102
- gpu_requested: bool,
103
- gpu_runtime_ok: bool,
104
- ) -> ProviderInfo:
105
- device_id = 0 if active_provider in GPU_PROVIDER_NAMES else None
106
- identity = resolve_device_identity(active_provider, device_id)
107
- return ProviderInfo(
108
- available_providers=available_providers,
109
- active_provider=active_provider,
110
- device=device,
111
- gpu_requested=gpu_requested,
112
- gpu_runtime_ok=gpu_runtime_ok,
113
- device_id=identity.device_id,
114
- device_name=identity.name,
115
- device_uuid=identity.uuid,
116
- device_verified=identity.verified,
117
- )
118
-
119
-
120
- def confirm_provider_device(provider_info: ProviderInfo) -> ProviderInfo:
121
- identity = confirm_device_identity(provider_info.active_provider, provider_info.device_id)
122
- return replace(
123
- provider_info,
124
- device_id=identity.device_id,
125
- device_name=identity.name,
126
- device_uuid=identity.uuid,
127
- device_verified=identity.verified,
128
- )
@@ -1,30 +0,0 @@
1
- numpy<3,>=1.26
2
- pillow<12,>=10
3
- opencv-python<5,>=4.10
4
- rapidocr==3.5.0
5
- transformers==4.55.4
6
- tokenizers==0.21.4
7
-
8
- [cpu]
9
-
10
- [cpu:python_version < "3.13"]
11
- onnxruntime<1.26,>=1.19.2
12
-
13
- [cpu:python_version >= "3.13"]
14
- onnxruntime<1.26,>=1.20
15
-
16
- [dev]
17
- ruff>=0.8
18
- build>=1.2
19
- twine>=5.0
20
-
21
- [gpu]
22
-
23
- [gpu:python_version < "3.13"]
24
- onnxruntime-gpu<1.26,>=1.19.2
25
-
26
- [gpu:python_version >= "3.13"]
27
- onnxruntime-gpu<1.26,>=1.20
28
-
29
- [pandoc]
30
- pypandoc>=1.15
File without changes
File without changes
File without changes