ducpy 3.5.0__tar.gz → 3.5.1__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 (61) hide show
  1. {ducpy-3.5.0 → ducpy-3.5.1}/Cargo.lock +1 -1
  2. {ducpy-3.5.0 → ducpy-3.5.1}/PKG-INFO +4 -2
  3. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducpy/crate/Cargo.toml +1 -1
  4. {ducpy-3.5.0 → ducpy-3.5.1}/pyproject.toml +3 -1
  5. ducpy-3.5.1/src/ducpy/search/image_ocr.py +189 -0
  6. ducpy-3.5.0/src/ducpy/search/image_ocr.py +0 -79
  7. {ducpy-3.5.0 → ducpy-3.5.1}/Cargo.toml +0 -0
  8. {ducpy-3.5.0 → ducpy-3.5.1}/LICENSE +0 -0
  9. {ducpy-3.5.0 → ducpy-3.5.1}/README.md +0 -0
  10. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducpy/crate/src/lib.rs +0 -0
  11. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/Cargo.toml +0 -0
  12. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/LICENSE +0 -0
  13. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/README.md +0 -0
  14. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/build.rs +0 -0
  15. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/duc.sql +0 -0
  16. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000000_to_3000001.sql +0 -0
  17. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000001_to_3000002.sql +0 -0
  18. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000002_to_3000003.sql +0 -0
  19. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000003_to_3000004.sql +0 -0
  20. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000004_to_3000005.sql +0 -0
  21. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/search.sql +0 -0
  22. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/version_control.sql +0 -0
  23. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/api/document.rs +0 -0
  24. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/api/meta.rs +0 -0
  25. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/api/mod.rs +0 -0
  26. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/api/version_control.rs +0 -0
  27. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/db/bootstrap.rs +0 -0
  28. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/db/mod.rs +0 -0
  29. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/db/native.rs +0 -0
  30. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/db/wasm.rs +0 -0
  31. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/lib.rs +0 -0
  32. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/parse.rs +0 -0
  33. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/serde_utils.rs +0 -0
  34. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/serialize.rs +0 -0
  35. {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/types.rs +0 -0
  36. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/__init__.py +0 -0
  37. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/__init__.py +0 -0
  38. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/block_instance_builder.py +0 -0
  39. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/block_utils.py +0 -0
  40. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/element_builders.py +0 -0
  41. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/mutate_builder.py +0 -0
  42. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/sql_builder.py +0 -0
  43. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/state_builders.py +0 -0
  44. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/style_builders.py +0 -0
  45. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/classes/DataStateClass.py +0 -0
  46. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/classes/ElementsClass.py +0 -0
  47. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/classes/__init__.py +0 -0
  48. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/enums.py +0 -0
  49. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/parse.py +0 -0
  50. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/search/__init__.py +0 -0
  51. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/search/search_elements.py +0 -0
  52. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/search/search_external_files.py +0 -0
  53. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/search/search_pdf.py +0 -0
  54. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/serialize.py +0 -0
  55. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/__init__.py +0 -0
  56. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/constants.py +0 -0
  57. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/convert.py +0 -0
  58. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/io.py +0 -0
  59. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/mutate_utils.py +0 -0
  60. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/rand_utils.py +0 -0
  61. {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy_native/__init__.py +0 -0
@@ -412,7 +412,7 @@ dependencies = [
412
412
 
413
413
  [[package]]
414
414
  name = "ducpy-native"
415
- version = "3.5.0"
415
+ version = "3.5.1"
416
416
  dependencies = [
417
417
  "duc",
418
418
  "pyo3",
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ducpy
3
- Version: 3.5.0
3
+ Version: 3.5.1
4
4
  Classifier: License :: OSI Approved :: MIT License
5
5
  Requires-Dist: nanoid>=2.0.0
6
6
  Requires-Dist: pypdf>=4.2.0
7
- Requires-Dist: rapidocr-onnxruntime>=1.3.24 ; extra == 'ocr'
7
+ Requires-Dist: rapidocr==3.8.1 ; extra == 'ocr'
8
+ Requires-Dist: onnxruntime ; extra == 'ocr'
9
+ Requires-Dist: huggingface-hub>=0.20.0 ; extra == 'ocr'
8
10
  Requires-Dist: numpy>=1.20.0 ; extra == 'ocr'
9
11
  Provides-Extra: ocr
10
12
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ducpy-native"
3
- version = "3.5.0"
3
+ version = "3.5.1"
4
4
  edition = "2021"
5
5
  description = "Python native extension for the duc file format (used by ducpy)"
6
6
  publish = false
@@ -22,7 +22,9 @@ classifiers = [
22
22
 
23
23
  [project.optional-dependencies]
24
24
  ocr = [
25
- "rapidocr-onnxruntime>=1.3.24",
25
+ "rapidocr==3.8.1",
26
+ "onnxruntime",
27
+ "huggingface-hub>=0.20.0",
26
28
  "numpy>=1.20.0",
27
29
  ]
28
30
 
@@ -0,0 +1,189 @@
1
+ """Server-side image OCR helpers for DUC search.
2
+
3
+ Uses ``rapidocr-onnxruntime`` with custom PP-OCRv6_tiny ONNX models when the ``ocr`` extra is installed, otherwise
4
+ OCR is gracefully skipped.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import io
10
+ import logging
11
+ from functools import lru_cache
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _RAPID_OCR_ENGINE: Any | None = None
17
+
18
+
19
+ @lru_cache(maxsize=1)
20
+ def server_side_ocr_available() -> bool:
21
+ """Return whether the ``ocr`` extra (RapidOCR) is installed and usable."""
22
+
23
+ try:
24
+ from rapidocr import RapidOCR # type: ignore
25
+ from huggingface_hub import snapshot_download # type: ignore # noqa: F401
26
+ from PIL import Image # type: ignore # noqa: F401
27
+ except Exception as exc:
28
+ logger.debug("OCR extra (rapidocr/huggingface-hub) unavailable: %s", exc)
29
+ return False
30
+
31
+ return True
32
+
33
+
34
+ def _get_local_cache_dir(repo_id: str) -> str | None:
35
+ import os
36
+ folder_name = "models--" + repo_id.replace("/", "--")
37
+ cache_dir = os.environ.get("HF_HUB_CACHE")
38
+ if not cache_dir:
39
+ hf_home = os.environ.get("HF_HOME")
40
+ if hf_home:
41
+ cache_dir = os.path.join(hf_home, "hub")
42
+ else:
43
+ cache_dir = os.path.expanduser("~/.cache/huggingface/hub")
44
+
45
+ repo_dir = os.path.join(cache_dir, folder_name)
46
+ if not os.path.isdir(repo_dir):
47
+ return None
48
+
49
+ # Read refs/main if it exists to get the current commit snapshot
50
+ refs_main = os.path.join(repo_dir, "refs", "main")
51
+ if os.path.isfile(refs_main):
52
+ try:
53
+ with open(refs_main, "r", encoding="utf-8") as f:
54
+ commit_hash = f.read().strip()
55
+ snapshot_dir = os.path.join(repo_dir, "snapshots", commit_hash)
56
+ if os.path.isdir(snapshot_dir):
57
+ return snapshot_dir
58
+ except Exception:
59
+ pass
60
+
61
+ # Fallback to the latest folder in snapshots/
62
+ snapshots_dir = os.path.join(repo_dir, "snapshots")
63
+ if os.path.isdir(snapshots_dir):
64
+ try:
65
+ subdirs = [os.path.join(snapshots_dir, d) for d in os.listdir(snapshots_dir)]
66
+ subdirs = [d for d in subdirs if os.path.isdir(d)]
67
+ if subdirs:
68
+ return max(subdirs, key=os.path.getmtime)
69
+ except Exception:
70
+ pass
71
+
72
+ return None
73
+
74
+
75
+ def _get_rapid_engine() -> Any:
76
+ global _RAPID_OCR_ENGINE
77
+ if _RAPID_OCR_ENGINE is None:
78
+ from rapidocr import RapidOCR # type: ignore
79
+ from rapidocr.utils.typings import EngineType # type: ignore
80
+ import os
81
+ import yaml
82
+
83
+ # Try to resolve cache locally first to avoid snapshot_download overhead (0.1ms vs 180ms)
84
+ det_dir = _get_local_cache_dir("PaddlePaddle/PP-OCRv6_tiny_det_onnx")
85
+ rec_dir = _get_local_cache_dir("PaddlePaddle/PP-OCRv6_tiny_rec_onnx")
86
+
87
+ # Fallback to snapshot_download if local files aren't fully resolved
88
+ if (
89
+ not det_dir
90
+ or not rec_dir
91
+ or not os.path.exists(os.path.join(det_dir, "inference.onnx"))
92
+ or not os.path.exists(os.path.join(rec_dir, "inference.onnx"))
93
+ ):
94
+ from huggingface_hub import snapshot_download # type: ignore
95
+ try:
96
+ det_dir = snapshot_download("PaddlePaddle/PP-OCRv6_tiny_det_onnx", local_files_only=True)
97
+ rec_dir = snapshot_download("PaddlePaddle/PP-OCRv6_tiny_rec_onnx", local_files_only=True)
98
+ except Exception:
99
+ det_dir = snapshot_download("PaddlePaddle/PP-OCRv6_tiny_det_onnx")
100
+ rec_dir = snapshot_download("PaddlePaddle/PP-OCRv6_tiny_rec_onnx")
101
+
102
+ det_model = os.path.join(det_dir, "inference.onnx")
103
+ rec_model = os.path.join(rec_dir, "inference.onnx")
104
+
105
+ # Load the dictionary from the model config to prevent IndexError
106
+ dict_path = os.path.join(rec_dir, "ppocrv6_keys.txt")
107
+ if not os.path.exists(dict_path):
108
+ yml_path = os.path.join(rec_dir, "inference.yml")
109
+ try:
110
+ with open(yml_path, "r", encoding="utf-8") as f:
111
+ cfg = yaml.safe_load(f)
112
+ char_list = cfg["PostProcess"]["character_dict"]
113
+ with open(dict_path, "w", encoding="utf-8") as f:
114
+ for char in char_list:
115
+ f.write(char + "\n")
116
+ except Exception as e:
117
+ logger.debug("Failed to extract character_dict from inference.yml: %s", e)
118
+ # Fallback to the en_dict.txt shipped in the repository if yaml parsing fails
119
+ dict_path = os.path.join(rec_dir, "en_dict.txt")
120
+
121
+ _RAPID_OCR_ENGINE = RapidOCR(
122
+ params={
123
+ "Global.use_cls": False,
124
+
125
+ "Det.model_path": det_model,
126
+ "Det.engine_type": EngineType.ONNXRUNTIME,
127
+
128
+ "Rec.model_path": rec_model,
129
+ "Rec.rec_keys_path": dict_path,
130
+ "Rec.engine_type": EngineType.ONNXRUNTIME,
131
+ }
132
+ )
133
+ return _RAPID_OCR_ENGINE
134
+
135
+
136
+
137
+ def extract_image_text_with_ocr(image_bytes: bytes, *, ocr_language: str) -> tuple[str, bool]:
138
+ """OCR an image using the ``ocr`` extra, or skip when unavailable."""
139
+
140
+ if not server_side_ocr_available():
141
+ return "", False
142
+
143
+ try:
144
+ import numpy as np # type: ignore
145
+ from PIL import Image # type: ignore
146
+ except Exception as exc:
147
+ logger.debug("Image OCR dependencies unavailable: %s", exc)
148
+ return "", False
149
+
150
+ try:
151
+ with Image.open(io.BytesIO(image_bytes)) as image_obj:
152
+ image_input = np.array(image_obj.convert("RGB"))
153
+
154
+ engine = _get_rapid_engine()
155
+ raw_res = engine(image_input)
156
+
157
+ if hasattr(raw_res, "txts"):
158
+ txts = raw_res.txts
159
+ elif isinstance(raw_res, tuple) and len(raw_res) == 2:
160
+ result, _ = raw_res
161
+ if hasattr(result, "txts"):
162
+ txts = result.txts
163
+ elif isinstance(result, (list, tuple)):
164
+ txts = [row[1] for row in result if isinstance(row, (tuple, list)) and len(row) >= 2]
165
+ else:
166
+ txts = None
167
+ elif isinstance(raw_res, (list, tuple)):
168
+ txts = [row[1] for row in raw_res if isinstance(row, (tuple, list)) and len(row) >= 2]
169
+ else:
170
+ txts = None
171
+
172
+ if not txts:
173
+ return "", False
174
+
175
+ lines: list[str] = []
176
+ for txt in txts:
177
+ value = str(txt or "").strip()
178
+ if value:
179
+ lines.append(value)
180
+ text = "\n".join(lines)
181
+ except Exception as exc:
182
+ logger.debug("Failed to OCR image bytes: %s", exc)
183
+ return "", False
184
+
185
+ return text, bool(text)
186
+
187
+
188
+ _server_side_ocr_available = server_side_ocr_available
189
+ _extract_image_text_with_ocr = extract_image_text_with_ocr
@@ -1,79 +0,0 @@
1
- """Server-side image OCR helpers for DUC search.
2
-
3
- Uses ``rapidocr-onnxruntime`` when the ``ocr`` extra is installed, otherwise
4
- OCR is gracefully skipped.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import io
10
- import logging
11
- from functools import lru_cache
12
- from typing import Any
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
- _RAPID_OCR_ENGINE: Any | None = None
17
-
18
-
19
- @lru_cache(maxsize=1)
20
- def server_side_ocr_available() -> bool:
21
- """Return whether the ``ocr`` extra (RapidOCR) is installed and usable."""
22
-
23
- try:
24
- from rapidocr_onnxruntime import RapidOCR # type: ignore
25
- from PIL import Image # type: ignore # noqa: F401
26
- except Exception as exc:
27
- logger.debug("OCR extra (rapidocr-onnxruntime) unavailable: %s", exc)
28
- return False
29
-
30
- return True
31
-
32
-
33
- def _get_rapid_engine() -> Any:
34
- global _RAPID_OCR_ENGINE
35
- if _RAPID_OCR_ENGINE is None:
36
- from rapidocr_onnxruntime import RapidOCR # type: ignore
37
-
38
- _RAPID_OCR_ENGINE = RapidOCR()
39
- return _RAPID_OCR_ENGINE
40
-
41
-
42
- def extract_image_text_with_ocr(image_bytes: bytes, *, ocr_language: str) -> tuple[str, bool]:
43
- """OCR an image using the ``ocr`` extra, or skip when unavailable."""
44
-
45
- if not server_side_ocr_available():
46
- return "", False
47
-
48
- try:
49
- import numpy as np # type: ignore
50
- from PIL import Image # type: ignore
51
- except Exception as exc:
52
- logger.debug("Image OCR dependencies unavailable: %s", exc)
53
- return "", False
54
-
55
- try:
56
- with Image.open(io.BytesIO(image_bytes)) as image_obj:
57
- image_input = np.array(image_obj.convert("RGB"))
58
-
59
- engine = _get_rapid_engine()
60
- result, _elapsed = engine(image_input)
61
- if not result:
62
- return "", False
63
-
64
- lines: list[str] = []
65
- for row in result:
66
- if isinstance(row, (tuple, list)) and len(row) >= 2:
67
- value = str(row[1] or "").strip()
68
- if value:
69
- lines.append(value)
70
- text = "\n".join(lines)
71
- except Exception as exc:
72
- logger.debug("Failed to OCR image bytes: %s", exc)
73
- return "", False
74
-
75
- return text, bool(text)
76
-
77
-
78
- _server_side_ocr_available = server_side_ocr_available
79
- _extract_image_text_with_ocr = extract_image_text_with_ocr
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