Changes committed

This commit is contained in:
2026-06-01 21:49:53 +05:30
parent 8537c653c1
commit 3163bb213e
387 changed files with 21940 additions and 107 deletions

View File

@@ -0,0 +1,210 @@
from __future__ import annotations
import uuid
from pathlib import Path
import cv2
import numpy as np
from paddleocr import PaddleOCR
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.logging_config import get_logger
from app.models.document import Document, DocumentPage
from app.repositories.document_repository import (
DocumentPageRepository,
DocumentRepository,
DocumentTextBlockRepository,
)
from app.storage.provider import get_storage_provider
logger = get_logger(__name__)
_ocr_instance: PaddleOCR | None = None
def get_ocr_engine() -> PaddleOCR:
"""Get or create singleton PaddleOCR instance."""
global _ocr_instance
if _ocr_instance is None:
_ocr_instance = PaddleOCR(
use_angle_cls=True,
lang=settings.ocr_language,
use_gpu=settings.ocr_use_gpu,
show_log=False,
det_db_thresh=0.3,
det_db_box_thresh=0.5,
rec_batch_num=6,
)
return _ocr_instance
class OCRService:
"""OCR processing service using PaddleOCR for scanned documents."""
def __init__(self, db: Session) -> None:
self.db = db
self.storage = get_storage_provider()
self.doc_repo = DocumentRepository(db)
self.page_repo = DocumentPageRepository(db)
self.text_block_repo = DocumentTextBlockRepository(db)
def process_image(self, document: Document) -> Document:
"""Process a scanned image document with OCR."""
file_path = self.storage.get_absolute_path(document.storage_path)
image = cv2.imread(file_path)
if image is None:
raise ValueError(f"Failed to read image: {file_path}")
height, width = image.shape[:2]
document.page_count = 1
document.is_scanned = True
# Save page image
image_filename = f"{document.id}_page_1.png"
image_bytes = cv2.imencode(".png", image)[1].tobytes()
image_path = self.storage.save_file(image_bytes, "images", image_filename)
# Create page record
doc_page = self.page_repo.create_page(
document_id=document.id,
page_number=1,
width=float(width),
height=float(height),
image_path=image_path,
)
# Run OCR
self._run_ocr_on_page(doc_page, file_path)
return document
def process_scanned_pdf(self, document: Document) -> Document:
"""Process a scanned PDF document - convert pages to images and OCR each."""
file_path = self.storage.get_absolute_path(document.storage_path)
try:
from pdf2image import convert_from_path
images = convert_from_path(file_path, dpi=300)
except Exception as e:
logger.error("pdf_to_image_failed", document_id=str(document.id), error=str(e))
raise
document.page_count = len(images)
document.is_scanned = True
for page_num, pil_image in enumerate(images, start=1):
# Convert PIL to OpenCV format
np_image = np.array(pil_image)
cv_image = cv2.cvtColor(np_image, cv2.COLOR_RGB2BGR)
height, width = cv_image.shape[:2]
# Save page image
image_filename = f"{document.id}_page_{page_num}.png"
image_bytes = cv2.imencode(".png", cv_image)[1].tobytes()
image_path = self.storage.save_file(image_bytes, "images", image_filename)
# Create page record
doc_page = self.page_repo.create_page(
document_id=document.id,
page_number=page_num,
width=float(width),
height=float(height),
image_path=image_path,
)
# Run OCR on saved image
temp_path = self.storage.get_absolute_path(image_path)
self._run_ocr_on_page(doc_page, temp_path)
return document
def _run_ocr_on_page(self, doc_page: DocumentPage, image_path: str) -> None:
"""Run PaddleOCR on a single page image and store results."""
ocr = get_ocr_engine()
try:
results = ocr.ocr(image_path, cls=True)
except Exception as e:
logger.error("ocr_failed", page_id=str(doc_page.id), error=str(e))
return
if not results or not results[0]:
logger.info("ocr_no_results", page_id=str(doc_page.id))
return
full_text_parts = []
sequence = 0
for line in results[0]:
if not line or len(line) < 2:
continue
bbox_points = line[0] # List of 4 corner points
text_info = line[1] # (text, confidence)
text = text_info[0] if isinstance(text_info, (list, tuple)) else str(text_info)
confidence = float(text_info[1]) if isinstance(text_info, (list, tuple)) and len(text_info) > 1 else 0.0
if not text.strip():
continue
# Convert bbox points to x, y, width, height
xs = [p[0] for p in bbox_points]
ys = [p[1] for p in bbox_points]
x = min(xs)
y = min(ys)
width = max(xs) - x
height = max(ys) - y
# Determine block type based on position
block_type = self._classify_text_block(
y, height, doc_page.height, text
)
self.text_block_repo.create_text_block(
page_id=doc_page.id,
text=text,
x=x,
y=y,
width=width,
height=height,
confidence=confidence,
block_type=block_type,
sequence=sequence,
)
full_text_parts.append(text)
sequence += 1
# Update page text content
doc_page.text_content = "\n".join(full_text_parts)
def _classify_text_block(
self,
y: float,
height: float,
page_height: float,
text: str,
) -> str:
"""Classify a text block as header, footer, watermark, or regular text."""
if page_height <= 0:
return "text"
relative_y = y / page_height
# Header: top 10%
if relative_y < 0.10:
return "header"
# Footer: bottom 10%
if relative_y > 0.90:
return "footer"
# Watermark detection heuristic: large text in center
if 0.3 < relative_y < 0.7 and height > page_height * 0.05:
# Check for common watermark words
watermark_keywords = {"confidential", "draft", "copy", "sample", "watermark", "void"}
if text.strip().lower() in watermark_keywords:
return "watermark"
return "text"