FinRAG / src /ingestion /ocr_handler.py
Harshavard21's picture
Initial commit
454f118
Raw History Blame Contribute Delete
5.11 kB
"""
src/ingestion/ocr_handler.py
=============================
OCR fallback for scanned pages using EasyOCR.
When is this used?
- Pages where PyMuPDF extracts little or no text (< 50 chars)
but the page clearly has content β†’ likely a scanned image page
- Some older BSE filings have scanned sections mixed with digital text
EasyOCR is chosen over pytesseract because:
- No external Tesseract binary install needed (pure Python)
- Better accuracy on mixed-language content (English + numbers + β‚Ή symbols)
- GPU acceleration support (uses your RTX 3050 if available)
Note: OCR is SLOW. Only triggered as fallback, not for every page.
"""
from __future__ import annotations
from pathlib import Path
from typing import Optional
import numpy as np
from src.utils.logger import logger
from config.settings import settings
# Lazy import β€” EasyOCR takes ~3s to initialize, only load when needed
_ocr_reader = None
def get_ocr_reader():
"""Lazy-initialize EasyOCR reader (GPU-aware)."""
global _ocr_reader
if _ocr_reader is None:
logger.info("Initializing EasyOCR reader (first time only, may take ~10s)...")
import easyocr
device = settings.get_device()
use_gpu = device == "cuda"
_ocr_reader = easyocr.Reader(
lang_list=["en"],
gpu=use_gpu,
verbose=False,
)
logger.info(f"EasyOCR ready | GPU: {use_gpu}")
return _ocr_reader
class OCRHandler:
"""
Handles OCR for scanned pages in PDFs.
Used as a fallback when PyMuPDF extracts insufficient text.
"""
# Threshold: if extracted text is shorter than this, try OCR
SPARSE_TEXT_THRESHOLD = 50 # characters
def is_sparse_page(self, extracted_text: str) -> bool:
"""
Determine if a page has insufficient extracted text and needs OCR.
Args:
extracted_text: Text already extracted by PyMuPDF for this page
Returns:
True if OCR fallback should be attempted
"""
# Clean whitespace to get actual content length
clean = extracted_text.strip().replace("\n", "").replace(" ", "")
return len(clean) < self.SPARSE_TEXT_THRESHOLD
def ocr_page(self, pdf_path: Path, page_number: int) -> str:
"""
Run OCR on a specific page from a PDF.
Process:
1. Render the page to a high-res image via PyMuPDF
2. Pass to EasyOCR
3. Return extracted text sorted by vertical position
Args:
pdf_path: Path to the PDF file
page_number: 1-indexed page number
Returns:
OCR-extracted text for the page (or empty string on failure)
"""
try:
import fitz
doc = fitz.open(str(pdf_path))
if page_number > len(doc):
doc.close()
return ""
page = doc[page_number - 1]
# Render at 2x DPI for better OCR accuracy (150 DPI β†’ 300 DPI equivalent)
mat = fitz.Matrix(2.0, 2.0)
pix = page.get_pixmap(matrix=mat, alpha=False)
img_array = np.frombuffer(pix.samples, dtype=np.uint8).reshape(
pix.height, pix.width, 3
)
doc.close()
reader = get_ocr_reader()
results = reader.readtext(img_array, detail=1, paragraph=False)
# Sort results top-to-bottom (by y-coordinate of bounding box)
results.sort(key=lambda r: r[0][0][1]) # top-left y coordinate
# Extract just the text, filter low confidence results
text_lines = []
for bbox, text, confidence in results:
if confidence > 0.3: # discard very low confidence
text_lines.append(text)
ocr_text = "\n".join(text_lines)
logger.debug(f"OCR page {page_number}: extracted {len(ocr_text)} chars")
return ocr_text
except Exception as e:
logger.warning(f"OCR failed on page {page_number} of {pdf_path.name}: {e}")
return ""
def ocr_pdf_sparse_pages(
self,
pdf_path: Path,
page_texts: dict[int, str],
) -> dict[int, str]:
"""
Run OCR only on pages that have sparse/missing text.
Args:
pdf_path: Path to the PDF
page_texts: Dict mapping page_number β†’ existing extracted text
Returns:
Dict of page_number β†’ OCR text (only for pages that needed OCR)
"""
ocr_results = {}
sparse_pages = [
page_num for page_num, text in page_texts.items()
if self.is_sparse_page(text)
]
if not sparse_pages:
logger.debug("No sparse pages detected β€” OCR not needed")
return {}
logger.info(f"Running OCR on {len(sparse_pages)} sparse pages: {sparse_pages}")
for page_num in sparse_pages:
ocr_text = self.ocr_page(pdf_path, page_num)
if ocr_text.strip():
ocr_results[page_num] = ocr_text
return ocr_results