provinans / src /endopath /classical_ocr.py
reversely's picture
Upload folder using huggingface_hub
2b208e3 verified
Raw History Blame Contribute Delete
3.1 kB
"""Classical OCR page reader: a `textsource.PageReader` that transcribes rendered pages on CPU with
Tesseract and makes no model call.
Shared by the two CPU backends: #84 pairs it with rules-only extraction, #85 with a local LLM extractor,
so it is defined once here and reused rather than duplicated. The default engine runs Tesseract through
`pytesseract`, which needs the `tesseract` binary installed on the host. The engine is injectable
(`ocr_page`), so a test passes a fake and a deployment could swap in another classical engine (PaddleOCR)
without touching the reader.
"""
from __future__ import annotations
from pathlib import Path
from typing import Callable, Optional
# The same paragraph spacing reocr.py joins pages with, so the concatenated text reads as one document
# and an evidence quote verifies as a verbatim substring of exactly this stored text (invariant 2).
PAGE_SEPARATOR = "\n\n"
def tesseract_ocr(path: Path) -> str:
"""Transcribe one page image with Tesseract. Imports `pytesseract` and Pillow lazily, so importing
this module needs neither the packages nor the tesseract binary until a page is actually read."""
import pytesseract
from PIL import Image
with Image.open(path) as image:
return pytesseract.image_to_string(image)
class ClassicalPageReader:
"""A `textsource.PageReader` that OCRs a report's rendered pages on CPU and joins them into one report
text, with no model call. The CPU backends read pages through this in place of the vision-model
reader. `ocr_page` transcribes one page image and defaults to Tesseract; a test injects a fake."""
def __init__(
self,
*,
ocr_page: Optional[Callable[[Path], str]] = None,
page_separator: str = PAGE_SEPARATOR,
) -> None:
self._ocr_page = ocr_page or tesseract_ocr
self._page_separator = page_separator
def read(self, page_images: list[Path]) -> str:
parts: list[str] = []
for path in page_images:
text = self._ocr_page(Path(path)).strip()
if text:
parts.append(text)
return self._page_separator.join(parts)
def tesseract_word_boxes(path: Path) -> tuple[list[dict], int, int]:
"""The words Tesseract reads on a page, each with its pixel box, plus the image size. Lazy imports,
so importing this module needs neither pytesseract nor the tesseract binary until a page is read."""
import pytesseract
from PIL import Image
from pytesseract import Output
with Image.open(path) as image:
width, height = image.size
data = pytesseract.image_to_data(image, output_type=Output.DICT)
words: list[dict] = []
for i, text in enumerate(data["text"]):
if text and text.strip():
words.append(
{
"text": text,
"left": data["left"][i],
"top": data["top"][i],
"width": data["width"][i],
"height": data["height"][i],
}
)
return words, width, height