ducpy 3.4.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.4.0 → ducpy-3.5.1}/Cargo.lock +1 -1
- {ducpy-3.4.0 → ducpy-3.5.1}/PKG-INFO +7 -1
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducpy/crate/Cargo.toml +1 -1
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/schema/duc.sql +1 -1
- ducpy-3.5.1/packages/ducrs/schema/migrations/3000004_to_3000005.sql +51 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/schema/search.sql +49 -1
- {ducpy-3.4.0 → ducpy-3.5.1}/pyproject.toml +15 -0
- ducpy-3.5.1/src/ducpy/search/__init__.py +19 -0
- ducpy-3.5.1/src/ducpy/search/image_ocr.py +189 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/search/search_elements.py +398 -89
- ducpy-3.5.1/src/ducpy/search/search_external_files.py +608 -0
- ducpy-3.5.1/src/ducpy/search/search_pdf.py +95 -0
- ducpy-3.4.0/src/ducpy/search/__init__.py +0 -13
- {ducpy-3.4.0 → ducpy-3.5.1}/Cargo.toml +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/LICENSE +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/README.md +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducpy/crate/src/lib.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/Cargo.toml +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/LICENSE +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/README.md +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/build.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000000_to_3000001.sql +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000001_to_3000002.sql +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000002_to_3000003.sql +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/schema/migrations/3000003_to_3000004.sql +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/schema/version_control.sql +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/api/document.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/api/meta.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/api/mod.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/api/version_control.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/db/bootstrap.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/db/mod.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/db/native.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/db/wasm.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/lib.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/parse.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/serde_utils.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/serialize.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/packages/ducrs/src/types.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/__init__.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/builders/__init__.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/builders/block_instance_builder.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/builders/block_utils.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/builders/element_builders.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/builders/mutate_builder.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/builders/sql_builder.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/builders/state_builders.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/builders/style_builders.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/classes/DataStateClass.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/classes/ElementsClass.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/classes/__init__.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/enums.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/parse.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/serialize.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/utils/__init__.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/utils/constants.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/utils/convert.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/utils/io.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/utils/mutate_utils.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy/utils/rand_utils.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.1}/src/ducpy_native/__init__.py +0 -0
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ducpy
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.1
|
|
4
4
|
Classifier: License :: OSI Approved :: MIT License
|
|
5
5
|
Requires-Dist: nanoid>=2.0.0
|
|
6
|
+
Requires-Dist: pypdf>=4.2.0
|
|
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'
|
|
10
|
+
Requires-Dist: numpy>=1.20.0 ; extra == 'ocr'
|
|
11
|
+
Provides-Extra: ocr
|
|
6
12
|
License-File: LICENSE
|
|
7
13
|
Summary: The library for the Duc 2D CAD file format
|
|
8
14
|
Author: Jorge Soares
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
-- "DUC_" in ASCII
|
|
2
2
|
-- Apply in order: duc.sql → version_control.sql → search.sql
|
|
3
3
|
PRAGMA application_id = 1146569567;
|
|
4
|
-
PRAGMA user_version =
|
|
4
|
+
PRAGMA user_version = 3000005;
|
|
5
5
|
PRAGMA journal_mode = WAL;
|
|
6
6
|
PRAGMA foreign_keys = ON;
|
|
7
7
|
PRAGMA synchronous = NORMAL;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- Migration: 3000003 → 3000004
|
|
2
|
+
-- Add searchable index for extracted external-file text (PDF content search).
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS external_file_text_index (
|
|
5
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
|
+
file_id TEXT NOT NULL,
|
|
7
|
+
revision_id TEXT NOT NULL,
|
|
8
|
+
mime_type TEXT NOT NULL,
|
|
9
|
+
extracted_text TEXT NOT NULL DEFAULT '',
|
|
10
|
+
has_ocr INTEGER NOT NULL DEFAULT 0,
|
|
11
|
+
updated INTEGER NOT NULL,
|
|
12
|
+
UNIQUE (file_id, revision_id)
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_external_file_text_index_file_id
|
|
16
|
+
ON external_file_text_index(file_id);
|
|
17
|
+
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_external_file_text_index_revision_id
|
|
19
|
+
ON external_file_text_index(revision_id);
|
|
20
|
+
|
|
21
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS search_external_file_text USING fts5(
|
|
22
|
+
extracted_text,
|
|
23
|
+
content='external_file_text_index',
|
|
24
|
+
content_rowid='id',
|
|
25
|
+
tokenize='unicode61 remove_diacritics 2',
|
|
26
|
+
prefix='2 3 4 5 6 7 8 9 10'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_ai
|
|
30
|
+
AFTER INSERT ON external_file_text_index BEGIN
|
|
31
|
+
INSERT INTO search_external_file_text(rowid, extracted_text)
|
|
32
|
+
VALUES (NEW.id, NEW.extracted_text);
|
|
33
|
+
END;
|
|
34
|
+
|
|
35
|
+
CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_ad
|
|
36
|
+
AFTER DELETE ON external_file_text_index BEGIN
|
|
37
|
+
INSERT INTO search_external_file_text(search_external_file_text, rowid, extracted_text)
|
|
38
|
+
VALUES ('delete', OLD.id, OLD.extracted_text);
|
|
39
|
+
END;
|
|
40
|
+
|
|
41
|
+
CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_au
|
|
42
|
+
AFTER UPDATE OF extracted_text ON external_file_text_index BEGIN
|
|
43
|
+
INSERT INTO search_external_file_text(search_external_file_text, rowid, extracted_text)
|
|
44
|
+
VALUES ('delete', OLD.id, OLD.extracted_text);
|
|
45
|
+
INSERT INTO search_external_file_text(rowid, extracted_text)
|
|
46
|
+
VALUES (NEW.id, NEW.extracted_text);
|
|
47
|
+
END;
|
|
48
|
+
|
|
49
|
+
INSERT INTO search_external_file_text(search_external_file_text) VALUES ('rebuild');
|
|
50
|
+
|
|
51
|
+
PRAGMA user_version = 3000005;
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
-- Triggers keep the FTS index automatically in sync on every INSERT/UPDATE/DELETE.
|
|
6
6
|
-- No application-level sync needed.
|
|
7
7
|
--
|
|
8
|
-
-- Depends on: duc.sql (elements, element_text, element_doc, element_model, blocks
|
|
8
|
+
-- Depends on: duc.sql (elements, element_text, element_doc, element_model, blocks,
|
|
9
|
+
-- external_files, external_file_revisions, external_file_revision_data, document_grid_config)
|
|
9
10
|
--
|
|
10
11
|
-- Query examples:
|
|
11
12
|
-- SELECT rowid, rank FROM search_elements WHERE search_elements MATCH 'motor';
|
|
@@ -129,6 +130,52 @@ CREATE TRIGGER IF NOT EXISTS trg_blocks_ai AFTER INSERT ON blocks BEGIN
|
|
|
129
130
|
INSERT INTO search_blocks(rowid, label, description)
|
|
130
131
|
VALUES (NEW.rowid, NEW.label, NEW.description);
|
|
131
132
|
END;
|
|
133
|
+
|
|
134
|
+
-- Extracted text index for external file revisions (used for PDF content search).
|
|
135
|
+
CREATE TABLE IF NOT EXISTS external_file_text_index (
|
|
136
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
137
|
+
file_id TEXT NOT NULL,
|
|
138
|
+
revision_id TEXT NOT NULL,
|
|
139
|
+
mime_type TEXT NOT NULL,
|
|
140
|
+
extracted_text TEXT NOT NULL DEFAULT '',
|
|
141
|
+
has_ocr INTEGER NOT NULL DEFAULT 0,
|
|
142
|
+
updated INTEGER NOT NULL,
|
|
143
|
+
UNIQUE (file_id, revision_id)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_external_file_text_index_file_id
|
|
147
|
+
ON external_file_text_index(file_id);
|
|
148
|
+
|
|
149
|
+
CREATE INDEX IF NOT EXISTS idx_external_file_text_index_revision_id
|
|
150
|
+
ON external_file_text_index(revision_id);
|
|
151
|
+
|
|
152
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS search_external_file_text USING fts5(
|
|
153
|
+
extracted_text,
|
|
154
|
+
content='external_file_text_index',
|
|
155
|
+
content_rowid='id',
|
|
156
|
+
tokenize='unicode61 remove_diacritics 2',
|
|
157
|
+
prefix='2 3 4 5 6 7 8 9 10'
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_ai
|
|
161
|
+
AFTER INSERT ON external_file_text_index BEGIN
|
|
162
|
+
INSERT INTO search_external_file_text(rowid, extracted_text)
|
|
163
|
+
VALUES (NEW.id, NEW.extracted_text);
|
|
164
|
+
END;
|
|
165
|
+
|
|
166
|
+
CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_ad
|
|
167
|
+
AFTER DELETE ON external_file_text_index BEGIN
|
|
168
|
+
INSERT INTO search_external_file_text(search_external_file_text, rowid, extracted_text)
|
|
169
|
+
VALUES ('delete', OLD.id, OLD.extracted_text);
|
|
170
|
+
END;
|
|
171
|
+
|
|
172
|
+
CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_au
|
|
173
|
+
AFTER UPDATE OF extracted_text ON external_file_text_index BEGIN
|
|
174
|
+
INSERT INTO search_external_file_text(search_external_file_text, rowid, extracted_text)
|
|
175
|
+
VALUES ('delete', OLD.id, OLD.extracted_text);
|
|
176
|
+
INSERT INTO search_external_file_text(rowid, extracted_text)
|
|
177
|
+
VALUES (NEW.id, NEW.extracted_text);
|
|
178
|
+
END;
|
|
132
179
|
CREATE TRIGGER IF NOT EXISTS trg_blocks_ad AFTER DELETE ON blocks BEGIN
|
|
133
180
|
INSERT INTO search_blocks(search_blocks, rowid, label, description)
|
|
134
181
|
VALUES ('delete', OLD.rowid, OLD.label, OLD.description);
|
|
@@ -146,3 +193,4 @@ INSERT INTO search_element_text(search_element_text) VALUES ('rebuild');
|
|
|
146
193
|
INSERT INTO search_element_doc(search_element_doc) VALUES ('rebuild');
|
|
147
194
|
INSERT INTO search_element_model(search_element_model) VALUES ('rebuild');
|
|
148
195
|
INSERT INTO search_blocks(search_blocks) VALUES ('rebuild');
|
|
196
|
+
INSERT INTO search_external_file_text(search_external_file_text) VALUES ('rebuild');
|
|
@@ -6,6 +6,7 @@ readme = "README.md"
|
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"nanoid>=2.0.0",
|
|
9
|
+
"pypdf>=4.2.0"
|
|
9
10
|
]
|
|
10
11
|
|
|
11
12
|
license = { text = "MIT" }
|
|
@@ -19,6 +20,14 @@ classifiers = [
|
|
|
19
20
|
"License :: OSI Approved :: MIT License",
|
|
20
21
|
]
|
|
21
22
|
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
ocr = [
|
|
25
|
+
"rapidocr==3.8.1",
|
|
26
|
+
"onnxruntime",
|
|
27
|
+
"huggingface-hub>=0.20.0",
|
|
28
|
+
"numpy>=1.20.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
22
31
|
[project.urls]
|
|
23
32
|
"Homepage" = "https://duc.ducflair.com"
|
|
24
33
|
"Source" = "https://github.com/ducflair/duc/tree/main/packages/ducpy"
|
|
@@ -44,11 +53,14 @@ where = ["src"]
|
|
|
44
53
|
|
|
45
54
|
[dependency-groups]
|
|
46
55
|
dev = [
|
|
56
|
+
"ducpy[ocr]",
|
|
57
|
+
"maturin>=1.0,<2.0",
|
|
47
58
|
"furo>=2024.8.6",
|
|
48
59
|
"sphinx>=8.1.3",
|
|
49
60
|
"sphinx-autoapi>=3.6.0",
|
|
50
61
|
"pytest>=8.3.5",
|
|
51
62
|
"rich>=13.0.0",
|
|
63
|
+
"numpy>=1.20.0",
|
|
52
64
|
"typst>=0.14.9",
|
|
53
65
|
"build123d>=0.10.0",
|
|
54
66
|
"ocp-vscode>=3.4.0",
|
|
@@ -60,3 +72,6 @@ dev = [
|
|
|
60
72
|
testpaths = ["src/tests"]
|
|
61
73
|
python_files = "test_*.py"
|
|
62
74
|
addopts = "-vv"
|
|
75
|
+
markers = [
|
|
76
|
+
"slow: marks tests as slow (OCR-heavy, etc.)",
|
|
77
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Search helpers for DUC SQLite databases."""
|
|
2
|
+
|
|
3
|
+
from .search_elements import (
|
|
4
|
+
DucElementSearchResult,
|
|
5
|
+
DucFileSearchResult,
|
|
6
|
+
DucSearchResponse,
|
|
7
|
+
DucSearchResult,
|
|
8
|
+
ExternalFileSearchTarget,
|
|
9
|
+
search_duc_elements,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"DucElementSearchResult",
|
|
14
|
+
"DucFileSearchResult",
|
|
15
|
+
"DucSearchResponse",
|
|
16
|
+
"DucSearchResult",
|
|
17
|
+
"ExternalFileSearchTarget",
|
|
18
|
+
"search_duc_elements",
|
|
19
|
+
]
|
|
@@ -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
|