Spaces:
Running
Running
Commit ·
6a876ee
1
Parent(s): a980424
feat: add paddle ocr
Browse files- Dockerfile +7 -0
- app/api/server.py +13 -0
- app/config.py +17 -0
- app/services/ocr_service.py +174 -26
- requirements.txt +14 -1
Dockerfile
CHANGED
|
@@ -16,6 +16,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 16 |
libssl-dev \
|
| 17 |
nodejs \
|
| 18 |
zlib1g-dev \
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
&& rm -rf /var/lib/apt/lists/*
|
| 20 |
|
| 21 |
RUN groupadd --gid 1000 appuser && \
|
|
|
|
| 16 |
libssl-dev \
|
| 17 |
nodejs \
|
| 18 |
zlib1g-dev \
|
| 19 |
+
# Runtime libs for PaddleOCR / opencv-contrib-python (cv2), mirroring the
|
| 20 |
+
# reference reconciliation-file-processing-service Dockerfile.
|
| 21 |
+
libglib2.0-0 \
|
| 22 |
+
libsm6 \
|
| 23 |
+
libxext6 \
|
| 24 |
+
libxrender-dev \
|
| 25 |
+
libgomp1 \
|
| 26 |
&& rm -rf /var/lib/apt/lists/*
|
| 27 |
|
| 28 |
RUN groupadd --gid 1000 appuser && \
|
app/api/server.py
CHANGED
|
@@ -111,6 +111,19 @@ async def lifespan(app: FastAPI):
|
|
| 111 |
_logger.info("Embedding service initialized with dims: %s", _embedding_service.loaded_dimensions)
|
| 112 |
_logger.info("Vector store service initialized with %d existing stores", len(_vector_store_service.list_stores()))
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
redis = create_redis_client(_settings.redis_url) if _settings.redis_url else None
|
| 115 |
scripts = await load_scripts(redis) if redis else {}
|
| 116 |
app.state.redis = redis
|
|
|
|
| 111 |
_logger.info("Embedding service initialized with dims: %s", _embedding_service.loaded_dimensions)
|
| 112 |
_logger.info("Vector store service initialized with %d existing stores", len(_vector_store_service.list_stores()))
|
| 113 |
|
| 114 |
+
# Warm the OCR engine at startup so the PP-OCRv6 models are downloaded and
|
| 115 |
+
# cached before the first request (avoids slow first OCR). Runs on the
|
| 116 |
+
# shared thread pool; failure is non-fatal (engine initializes lazily on
|
| 117 |
+
# first use).
|
| 118 |
+
try:
|
| 119 |
+
from app.core.thread_pool import run_in_executor
|
| 120 |
+
from app.services.ocr_service import warmup as warmup_ocr
|
| 121 |
+
|
| 122 |
+
await run_in_executor(warmup_ocr)
|
| 123 |
+
_logger.info("OCR engine warmed up at startup")
|
| 124 |
+
except Exception as exc:
|
| 125 |
+
_logger.warning("OCR engine warm-up failed (will lazy-init on first use): %s", exc)
|
| 126 |
+
|
| 127 |
redis = create_redis_client(_settings.redis_url) if _settings.redis_url else None
|
| 128 |
scripts = await load_scripts(redis) if redis else {}
|
| 129 |
app.state.redis = redis
|
app/config.py
CHANGED
|
@@ -31,6 +31,8 @@ class Settings(BaseSettings):
|
|
| 31 |
max_batch_files: int = 10
|
| 32 |
max_batch_urls: int = 20
|
| 33 |
|
|
|
|
|
|
|
| 34 |
ocr_det_cuda: bool = False
|
| 35 |
ocr_det_dml: bool = False
|
| 36 |
ocr_cls_cuda: bool = False
|
|
@@ -38,6 +40,21 @@ class Settings(BaseSettings):
|
|
| 38 |
ocr_rec_cuda: bool = False
|
| 39 |
ocr_rec_dml: bool = False
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
self_ping_url: str = "https://aetherbase-llm-ready-data.hf.space/ping"
|
| 42 |
spacy_model: str = "en_core_web_sm"
|
| 43 |
|
|
|
|
| 31 |
max_batch_files: int = 10
|
| 32 |
max_batch_urls: int = 20
|
| 33 |
|
| 34 |
+
# Legacy RapidOCR flags -- retained for the commented-out RapidOCR engine
|
| 35 |
+
# (see app/services/ocr_service.py). No longer used at runtime.
|
| 36 |
ocr_det_cuda: bool = False
|
| 37 |
ocr_det_dml: bool = False
|
| 38 |
ocr_cls_cuda: bool = False
|
|
|
|
| 40 |
ocr_rec_cuda: bool = False
|
| 41 |
ocr_rec_dml: bool = False
|
| 42 |
|
| 43 |
+
# PaddleOCR PP-OCRv6 engine (default: PP-OCRv6 small tier on ONNX Runtime,
|
| 44 |
+
# mirroring the reference reconciliation-file-processing-service setup).
|
| 45 |
+
ocr_engine: str = "onnxruntime" # paddle | paddle_static | paddle_dynamic | onnxruntime | transformers
|
| 46 |
+
ocr_device: str = "cpu"
|
| 47 |
+
ocr_lang: Optional[str] = None # e.g. "en"; None uses the model defaults
|
| 48 |
+
ocr_det_model_name: str = "PP-OCRv6_small_det"
|
| 49 |
+
ocr_rec_model_name: str = "PP-OCRv6_small_rec"
|
| 50 |
+
ocr_use_doc_orientation_classify: bool = False
|
| 51 |
+
ocr_use_doc_unwarping: bool = False
|
| 52 |
+
ocr_use_textline_orientation: bool = True
|
| 53 |
+
# Max concurrent predictions on the shared PaddleOCR engine. PaddleX
|
| 54 |
+
# pipelines are not documented thread-safe, so this serializes inference
|
| 55 |
+
# on the shared instance (not a new worker pool).
|
| 56 |
+
ocr_max_concurrent: int = 1
|
| 57 |
+
|
| 58 |
self_ping_url: str = "https://aetherbase-llm-ready-data.hf.space/ping"
|
| 59 |
spacy_model: str = "en_core_web_sm"
|
| 60 |
|
app/services/ocr_service.py
CHANGED
|
@@ -17,34 +17,113 @@ _settings = get_settings()
|
|
| 17 |
_lock = threading.Lock()
|
| 18 |
_engine = None
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
def _get_engine():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
global _engine
|
| 23 |
if _engine is None:
|
| 24 |
with _lock:
|
| 25 |
if _engine is None:
|
| 26 |
-
from
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
)
|
| 33 |
return _engine
|
| 34 |
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
def _to_numpy(source) -> Union[np.ndarray, str]:
|
| 37 |
if isinstance(source, Image.Image):
|
| 38 |
-
|
| 39 |
-
if img.mode not in ("RGB", "L", "RGBA"):
|
| 40 |
-
img = img.convert("RGB")
|
| 41 |
-
return np.array(img)
|
| 42 |
|
| 43 |
if isinstance(source, (bytes, bytearray)):
|
| 44 |
img = Image.open(io.BytesIO(source))
|
| 45 |
-
|
| 46 |
-
img = img.convert("RGB")
|
| 47 |
-
return np.array(img)
|
| 48 |
|
| 49 |
if isinstance(source, str):
|
| 50 |
parsed = urlparse(source)
|
|
@@ -53,12 +132,14 @@ def _to_numpy(source) -> Union[np.ndarray, str]:
|
|
| 53 |
resp = httpx.get(source, follow_redirects=True, timeout=30)
|
| 54 |
resp.raise_for_status()
|
| 55 |
img = Image.open(io.BytesIO(resp.content))
|
| 56 |
-
|
| 57 |
-
img = img.convert("RGB")
|
| 58 |
-
return np.array(img)
|
| 59 |
return source
|
| 60 |
|
| 61 |
if isinstance(source, np.ndarray):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
return source
|
| 63 |
|
| 64 |
raise TypeError(
|
|
@@ -66,6 +147,47 @@ def _to_numpy(source) -> Union[np.ndarray, str]:
|
|
| 66 |
)
|
| 67 |
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
def ocr_image(
|
| 70 |
source,
|
| 71 |
*,
|
|
@@ -74,18 +196,32 @@ def ocr_image(
|
|
| 74 |
use_rec: bool = True,
|
| 75 |
text_score: float = 0.5,
|
| 76 |
) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
engine = _get_engine()
|
| 78 |
img = _to_numpy(source)
|
| 79 |
-
|
| 80 |
-
img
|
| 81 |
-
|
| 82 |
-
use_cls=use_cls,
|
| 83 |
-
use_rec=use_rec,
|
| 84 |
-
text_score=text_score,
|
| 85 |
-
)
|
| 86 |
-
if not result:
|
| 87 |
-
return ""
|
| 88 |
-
lines = [item[1] for item in result if len(item) > 1 and item[1]]
|
| 89 |
return "\n".join(lines)
|
| 90 |
|
| 91 |
|
|
@@ -119,3 +255,15 @@ def ocr_pdf(source: Union[str, bytes], *, dpi: int = 150) -> str:
|
|
| 119 |
class OCRService:
|
| 120 |
def __init__(self) -> None:
|
| 121 |
self._engine = _get_engine()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
_lock = threading.Lock()
|
| 18 |
_engine = None
|
| 19 |
|
| 20 |
+
# ---------------------------------------------------------------------------
|
| 21 |
+
# Legacy RapidOCR implementation (RapidOCR) -- commented out, NOT deleted.
|
| 22 |
+
#
|
| 23 |
+
# It used the ONNX Runtime based `rapidocr_onnxruntime` package with per-module
|
| 24 |
+
# CUDA/DML flags. Kept here for reference / easy rollback.
|
| 25 |
+
#
|
| 26 |
+
# def _get_engine():
|
| 27 |
+
# global _engine
|
| 28 |
+
# if _engine is None:
|
| 29 |
+
# with _lock:
|
| 30 |
+
# if _engine is None:
|
| 31 |
+
# from rapidocr_onnxruntime import RapidOCR
|
| 32 |
+
# _engine = RapidOCR(
|
| 33 |
+
# Det={"use_cuda": _settings.ocr_det_cuda, "use_dml": _settings.ocr_det_dml},
|
| 34 |
+
# Cls={"use_cuda": _settings.ocr_cls_cuda, "use_dml": _settings.ocr_cls_dml},
|
| 35 |
+
# Rec={"use_cuda": _settings.ocr_rec_cuda, "use_dml": _settings.ocr_rec_dml},
|
| 36 |
+
# print_verbose=False,
|
| 37 |
+
# )
|
| 38 |
+
# return _engine
|
| 39 |
+
#
|
| 40 |
+
# def ocr_image(source, *, use_det=True, use_cls=True, use_rec=True, text_score=0.5) -> str:
|
| 41 |
+
# engine = _get_engine()
|
| 42 |
+
# img = _to_numpy(source)
|
| 43 |
+
# result, _ = engine(
|
| 44 |
+
# img,
|
| 45 |
+
# use_det=use_det,
|
| 46 |
+
# use_cls=use_cls,
|
| 47 |
+
# use_rec=use_rec,
|
| 48 |
+
# text_score=text_score,
|
| 49 |
+
# )
|
| 50 |
+
# if not result:
|
| 51 |
+
# return ""
|
| 52 |
+
# lines = [item[1] for item in result if len(item) > 1 and item[1]]
|
| 53 |
+
# return "\n".join(lines)
|
| 54 |
+
# ---------------------------------------------------------------------------
|
| 55 |
+
|
| 56 |
+
# Guards the shared PaddleOCR pipeline instance. PaddleOCR/PaddleX pipelines are
|
| 57 |
+
# not documented as thread-safe for concurrent predict() calls, so OCR inference
|
| 58 |
+
# is serialized on the shared engine (CPU-bound anyway). This is NOT a new worker
|
| 59 |
+
# pool -- it merely limits concurrent use of the single shared model instance.
|
| 60 |
+
_infer_semaphore = threading.BoundedSemaphore(
|
| 61 |
+
max(1, _settings.ocr_max_concurrent)
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
|
| 65 |
def _get_engine():
|
| 66 |
+
"""Return the shared PaddleOCR PP-OCRv6 Small engine (lazy singleton).
|
| 67 |
+
|
| 68 |
+
Uses the PP-OCRv6 *small* tier models (PP-OCRv6_small_det /
|
| 69 |
+
PP-OCRv6_small_rec) shipped with PaddleOCR 3.7.0, executed on the ONNX
|
| 70 |
+
Runtime backend (engine="onnxruntime"). This mirrors the OCR benchmark /
|
| 71 |
+
reference reconciliation-file-processing-service, which pairs
|
| 72 |
+
``paddleocr==3.7.0`` with PP-OCRv6-small models and the ONNX Runtime engine.
|
| 73 |
+
"""
|
| 74 |
global _engine
|
| 75 |
if _engine is None:
|
| 76 |
with _lock:
|
| 77 |
if _engine is None:
|
| 78 |
+
from paddleocr import PaddleOCR
|
| 79 |
+
|
| 80 |
+
kwargs: dict = {
|
| 81 |
+
"text_detection_model_name": _settings.ocr_det_model_name,
|
| 82 |
+
"text_recognition_model_name": _settings.ocr_rec_model_name,
|
| 83 |
+
"use_doc_orientation_classify": _settings.ocr_use_doc_orientation_classify,
|
| 84 |
+
"use_doc_unwarping": _settings.ocr_use_doc_unwarping,
|
| 85 |
+
"use_textline_orientation": _settings.ocr_use_textline_orientation,
|
| 86 |
+
"engine": _settings.ocr_engine,
|
| 87 |
+
"device": _settings.ocr_device,
|
| 88 |
+
}
|
| 89 |
+
if _settings.ocr_lang:
|
| 90 |
+
kwargs["lang"] = _settings.ocr_lang
|
| 91 |
+
_engine = PaddleOCR(**kwargs)
|
| 92 |
+
_logger.info(
|
| 93 |
+
"PaddleOCR engine ready (engine=%s device=%s det=%s rec=%s)",
|
| 94 |
+
_settings.ocr_engine,
|
| 95 |
+
_settings.ocr_device,
|
| 96 |
+
_settings.ocr_det_model_name,
|
| 97 |
+
_settings.ocr_rec_model_name,
|
| 98 |
)
|
| 99 |
return _engine
|
| 100 |
|
| 101 |
|
| 102 |
+
def _to_rgb(img: Image.Image) -> Image.Image:
|
| 103 |
+
"""Normalise an image to 3-channel RGB.
|
| 104 |
+
|
| 105 |
+
PaddleOCR's recognition model asserts a 3-channel input (``imgC ==
|
| 106 |
+
img.shape[2]``), so RGBA (4-channel) and palette/grayscale inputs must be
|
| 107 |
+
converted -- unlike RapidOCR, which accepted RGBA arrays as-is.
|
| 108 |
+
"""
|
| 109 |
+
if img.mode == "RGBA":
|
| 110 |
+
# Flatten alpha onto a white background: standard for scanned documents
|
| 111 |
+
# and avoids black halos around transparent regions.
|
| 112 |
+
background = Image.new("RGB", img.size, (255, 255, 255))
|
| 113 |
+
background.paste(img, mask=img.split()[-1])
|
| 114 |
+
return background
|
| 115 |
+
if img.mode != "RGB":
|
| 116 |
+
return img.convert("RGB")
|
| 117 |
+
return img
|
| 118 |
+
|
| 119 |
+
|
| 120 |
def _to_numpy(source) -> Union[np.ndarray, str]:
|
| 121 |
if isinstance(source, Image.Image):
|
| 122 |
+
return np.array(_to_rgb(source))
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
if isinstance(source, (bytes, bytearray)):
|
| 125 |
img = Image.open(io.BytesIO(source))
|
| 126 |
+
return np.array(_to_rgb(img))
|
|
|
|
|
|
|
| 127 |
|
| 128 |
if isinstance(source, str):
|
| 129 |
parsed = urlparse(source)
|
|
|
|
| 132 |
resp = httpx.get(source, follow_redirects=True, timeout=30)
|
| 133 |
resp.raise_for_status()
|
| 134 |
img = Image.open(io.BytesIO(resp.content))
|
| 135 |
+
return np.array(_to_rgb(img))
|
|
|
|
|
|
|
| 136 |
return source
|
| 137 |
|
| 138 |
if isinstance(source, np.ndarray):
|
| 139 |
+
# Defensive: PaddleOCR needs 3-channel input. If a 4-channel array was
|
| 140 |
+
# passed directly, drop the alpha channel.
|
| 141 |
+
if source.ndim == 3 and source.shape[2] == 4:
|
| 142 |
+
source = source[:, :, :3]
|
| 143 |
return source
|
| 144 |
|
| 145 |
raise TypeError(
|
|
|
|
| 147 |
)
|
| 148 |
|
| 149 |
|
| 150 |
+
def _append_rec_texts(item: dict, texts: list[str], text_score: float) -> None:
|
| 151 |
+
"""Append recognized lines from one PaddleOCR result dict.
|
| 152 |
+
|
| 153 |
+
``rec_texts`` holds the recognized strings and ``rec_scores`` the matching
|
| 154 |
+
confidences (same index order). Lines below ``text_score`` are dropped,
|
| 155 |
+
mirroring the old RapidOCR ``text_score`` behaviour.
|
| 156 |
+
"""
|
| 157 |
+
rec_texts = item.get("rec_texts") or []
|
| 158 |
+
rec_scores = item.get("rec_scores")
|
| 159 |
+
for idx, text in enumerate(rec_texts):
|
| 160 |
+
if not text:
|
| 161 |
+
continue
|
| 162 |
+
if rec_scores is not None and idx < len(rec_scores):
|
| 163 |
+
try:
|
| 164 |
+
score = float(rec_scores[idx])
|
| 165 |
+
except (TypeError, ValueError):
|
| 166 |
+
score = text_score
|
| 167 |
+
if score < text_score:
|
| 168 |
+
continue
|
| 169 |
+
texts.append(text)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _flatten_result(result, *, text_score: float = 0.5) -> list[str]:
|
| 173 |
+
"""Flatten a PaddleOCR ``predict()`` result into recognized text lines.
|
| 174 |
+
|
| 175 |
+
Handles both the single-dict form and the nested list-of-dicts form the
|
| 176 |
+
pipeline can return (the reference project iterates the same shapes).
|
| 177 |
+
"""
|
| 178 |
+
texts: list[str] = []
|
| 179 |
+
if not result:
|
| 180 |
+
return texts
|
| 181 |
+
for item in result:
|
| 182 |
+
if isinstance(item, dict):
|
| 183 |
+
_append_rec_texts(item, texts, text_score)
|
| 184 |
+
elif isinstance(item, (list, tuple)):
|
| 185 |
+
for sub in item:
|
| 186 |
+
if isinstance(sub, dict):
|
| 187 |
+
_append_rec_texts(sub, texts, text_score)
|
| 188 |
+
return texts
|
| 189 |
+
|
| 190 |
+
|
| 191 |
def ocr_image(
|
| 192 |
source,
|
| 193 |
*,
|
|
|
|
| 196 |
use_rec: bool = True,
|
| 197 |
text_score: float = 0.5,
|
| 198 |
) -> str:
|
| 199 |
+
"""Run OCR on an image and return the recognized text lines.
|
| 200 |
+
|
| 201 |
+
API contract is unchanged from the RapidOCR implementation. Note: the
|
| 202 |
+
PaddleOCR 3.x pipeline always performs detection + recognition together;
|
| 203 |
+
``use_det`` / ``use_cls`` / ``use_rec`` are accepted for backward
|
| 204 |
+
compatibility. ``use_cls`` maps to the textline-orientation module, which is
|
| 205 |
+
configured at engine initialisation (``ocr_use_textline_orientation``).
|
| 206 |
+
"""
|
| 207 |
+
if not (use_det and use_rec):
|
| 208 |
+
_logger.warning(
|
| 209 |
+
"ocr_image: use_det/use_rec are no-ops with the PaddleOCR pipeline "
|
| 210 |
+
"(detection+recognition always run); got use_det=%s use_rec=%s",
|
| 211 |
+
use_det,
|
| 212 |
+
use_rec,
|
| 213 |
+
)
|
| 214 |
+
if not use_cls:
|
| 215 |
+
_logger.warning(
|
| 216 |
+
"ocr_image: use_cls=%s is ignored; textline orientation is fixed at "
|
| 217 |
+
"engine init via ocr_use_textline_orientation",
|
| 218 |
+
use_cls,
|
| 219 |
+
)
|
| 220 |
engine = _get_engine()
|
| 221 |
img = _to_numpy(source)
|
| 222 |
+
with _infer_semaphore:
|
| 223 |
+
result = engine.predict(img)
|
| 224 |
+
lines = _flatten_result(result, text_score=text_score)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
return "\n".join(lines)
|
| 226 |
|
| 227 |
|
|
|
|
| 255 |
class OCRService:
|
| 256 |
def __init__(self) -> None:
|
| 257 |
self._engine = _get_engine()
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def warmup() -> None:
|
| 261 |
+
"""Force model download + engine initialisation at startup.
|
| 262 |
+
|
| 263 |
+
PaddleOCR downloads the PP-OCRv6 small ONNX models and caches them under
|
| 264 |
+
``~/.paddlex/official_models`` on first use. Calling this during the app
|
| 265 |
+
lifespan (in the shared thread pool) means the first OCR request never
|
| 266 |
+
pays the download/init cost.
|
| 267 |
+
"""
|
| 268 |
+
_get_engine()
|
| 269 |
+
_logger.info("PaddleOCR engine warmed up (models downloaded and cached)")
|
requirements.txt
CHANGED
|
@@ -10,12 +10,25 @@ aiohttp==3.11.13
|
|
| 10 |
# --- Media / file processing ---
|
| 11 |
numpy==2.2.6
|
| 12 |
pillow==10.3.0
|
| 13 |
-
opencv-
|
|
|
|
|
|
|
|
|
|
| 14 |
pypdfium2==4.30.0
|
|
|
|
|
|
|
| 15 |
rapidocr-onnxruntime==1.4.4
|
| 16 |
onnxruntime==1.20.1
|
| 17 |
markitdown[all]==0.1.5
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# --- Data / analysis ---
|
| 20 |
pandas==3.0.1
|
| 21 |
matplotlib==3.9.2
|
|
|
|
| 10 |
# --- Media / file processing ---
|
| 11 |
numpy==2.2.6
|
| 12 |
pillow==10.3.0
|
| 13 |
+
# Aligned with paddlex[ocr-core]'s opencv-contrib-python==4.10.0.84 pin (both
|
| 14 |
+
# provide cv2; same version avoids install-time file conflicts). Mirrors the
|
| 15 |
+
# reference reconciliation-file-processing-service requirements.
|
| 16 |
+
opencv-python-headless==4.10.0.84
|
| 17 |
pypdfium2==4.30.0
|
| 18 |
+
# Legacy RapidOCR engine -- kept installed for the commented-out implementation
|
| 19 |
+
# in app/services/ocr_service.py (enables easy rollback).
|
| 20 |
rapidocr-onnxruntime==1.4.4
|
| 21 |
onnxruntime==1.20.1
|
| 22 |
markitdown[all]==0.1.5
|
| 23 |
|
| 24 |
+
# --- OCR: PaddleOCR PP-OCRv6 Small on ONNX Runtime ---
|
| 25 |
+
# PaddleOCR 3.7.0 ships the PP-OCRv6 model family (tiny/small/medium). The
|
| 26 |
+
# engine runs on ONNX Runtime (engine="onnxruntime"), which does NOT require the
|
| 27 |
+
# PaddlePaddle framework -- PaddleX 3.7 avoids importing `paddle` unless a
|
| 28 |
+
# paddle_static/paddle_dynamic engine is selected. PP-OCRv6-small ONNX models
|
| 29 |
+
# are downloaded on first use to ~/.paddlex/official_models.
|
| 30 |
+
paddleocr==3.7.0
|
| 31 |
+
|
| 32 |
# --- Data / analysis ---
|
| 33 |
pandas==3.0.1
|
| 34 |
matplotlib==3.9.2
|