This commit is contained in:
Sorin Savu
2026-07-13 09:51:28 +03:00
parent 0ad947ba56
commit dcc6bba61b
10 changed files with 1612 additions and 8 deletions
+56
View File
@@ -0,0 +1,56 @@
"""Pluggable OCR.
The engine is chosen by the OCR_ENGINE env var:
tesseract (default) -- pytesseract + Pillow + the system `tesseract` binary
none -- OCR disabled; images are marked 'skipped'
Swapping in another engine later (e.g. a Claude-vision call) is just another
branch in `run_ocr` behind the same signature. `run_ocr` raises OCRUnavailable
when the configured engine's dependencies are missing (the worker then marks the
row 'skipped' rather than 'failed'), and lets real OCR errors propagate.
"""
from __future__ import annotations
import io
import os
class OCRUnavailable(RuntimeError):
"""The configured OCR engine can't run (missing binary / library)."""
def engine() -> str:
return os.environ.get("OCR_ENGINE", "tesseract").lower()
def enabled() -> bool:
return engine() != "none"
def run_ocr(data: bytes, mime: str) -> str:
"""Return extracted text for an image. Raises OCRUnavailable if the engine
is missing, or any other exception on a genuine OCR failure."""
eng = engine()
if eng == "none":
raise OCRUnavailable("OCR_ENGINE=none")
if eng == "tesseract":
return _tesseract(data)
raise OCRUnavailable(f"unknown OCR_ENGINE={eng!r}")
def _tesseract(data: bytes) -> str:
try:
import pytesseract
from PIL import Image
except ImportError as e:
raise OCRUnavailable(f"pytesseract/Pillow not installed: {e}") from e
try:
img = Image.open(io.BytesIO(data))
except Exception as e: # not a decodable image
raise OCRUnavailable(f"not a decodable image: {e}") from e
try:
return pytesseract.image_to_string(img)
except pytesseract.TesseractNotFoundError as e:
raise OCRUnavailable("tesseract binary not found on PATH") from e