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.
- {ducpy-3.5.0 → ducpy-3.5.1}/Cargo.lock +1 -1
- {ducpy-3.5.0 → ducpy-3.5.1}/PKG-INFO +4 -2
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducpy/crate/Cargo.toml +1 -1
- {ducpy-3.5.0 → ducpy-3.5.1}/pyproject.toml +3 -1
- ducpy-3.5.1/src/ducpy/search/image_ocr.py +189 -0
- ducpy-3.5.0/src/ducpy/search/image_ocr.py +0 -79
- {ducpy-3.5.0 → ducpy-3.5.1}/Cargo.toml +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/LICENSE +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/README.md +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducpy/crate/src/lib.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/Cargo.toml +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/LICENSE +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/README.md +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/build.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/duc.sql +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000000_to_3000001.sql +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000001_to_3000002.sql +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000002_to_3000003.sql +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000003_to_3000004.sql +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000004_to_3000005.sql +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/search.sql +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/schema/version_control.sql +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/api/document.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/api/meta.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/api/mod.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/api/version_control.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/db/bootstrap.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/db/mod.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/db/native.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/db/wasm.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/lib.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/parse.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/serde_utils.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/serialize.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/packages/ducrs/src/types.rs +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/__init__.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/__init__.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/block_instance_builder.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/block_utils.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/element_builders.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/mutate_builder.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/sql_builder.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/state_builders.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/builders/style_builders.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/classes/DataStateClass.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/classes/ElementsClass.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/classes/__init__.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/enums.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/parse.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/search/__init__.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/search/search_elements.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/search/search_external_files.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/search/search_pdf.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/serialize.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/__init__.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/constants.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/convert.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/io.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/mutate_utils.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy/utils/rand_utils.py +0 -0
- {ducpy-3.5.0 → ducpy-3.5.1}/src/ducpy_native/__init__.py +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ducpy
|
|
3
|
-
Version: 3.5.
|
|
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
|
|
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
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|