feat: Agregar soporte OCR con Tesseract para PDFs escaneados

- Integrar Tesseract OCR para leer PDFs escaneados automáticamente
- Detectar automáticamente si el PDF tiene texto o requiere OCR
- Agregar servicio ocr_service.py con funciones de OCR
- Actualizar Dockerfile con tesseract-ocr, tesseract-ocr-spa y poppler-utils
- Agregar variables de configuración OCR (OCR_ENABLED, OCR_LANGUAGE, OCR_DPI, OCR_TIMEOUT)
- Crear endpoint de debug para ver texto extraído (/api/v1/debug/extract-text)
- Agregar scripts de instalación y prueba (install_ocr.ps1, test_ocr.py, debug_pdf.ps1)
- Documentación completa (OCR_SETUP.md, DOCKER_OCR.md, COMO_PROBAR.md)
- Actualizar docker-compose.yml con variables de entorno OCR
- Modificar pdf_text.py para usar OCR cuando sea necesario
- Actualizar requirements.txt con pytesseract, Pillow, pdf2image
This commit is contained in:
Ernesto Herrera
2026-03-04 08:21:41 -07:00
parent 068d859f42
commit fcc516c9b3
18 changed files with 1694 additions and 14 deletions

View File

@@ -1,9 +1,10 @@
"""PDF text extraction service using PyMuPDF and pdfplumber."""
"""PDF text extraction service using PyMuPDF, pdfplumber, and OCR."""
import fitz # PyMuPDF
import pdfplumber
import logging
from typing import Tuple, Optional
from io import BytesIO
from .ocr_service import extract_text_with_ocr, is_pdf_scanned, OCRError
logger = logging.getLogger(__name__)
@@ -82,16 +83,19 @@ def extract_text_with_pdfplumber(pdf_bytes: bytes) -> Tuple[str, int]:
raise PDFExtractionError(f"pdfplumber extraction failed: {str(e)}")
def extract_text_from_pdf(pdf_bytes: bytes) -> Tuple[str, int, str]:
def extract_text_from_pdf(pdf_bytes: bytes, enable_ocr: bool = True, ocr_lang: str = "spa") -> Tuple[str, int, str]:
"""
Extract text from PDF using available methods.
Tries PyMuPDF first, falls back to pdfplumber.
Tries PyMuPDF first, falls back to pdfplumber, and uses OCR for scanned PDFs.
Args:
pdf_bytes: PDF file content as bytes
enable_ocr: Whether to use OCR for scanned PDFs (default: True)
ocr_lang: Language for OCR - "spa" for Spanish, "eng" for English (default: "spa")
Returns:
Tuple of (extracted_text, page_count, method_used)
method_used can be: "pymupdf", "pdfplumber", "ocr"
Raises:
PDFExtractionError: If all extraction methods fail
@@ -99,14 +103,41 @@ def extract_text_from_pdf(pdf_bytes: bytes) -> Tuple[str, int, str]:
# Try PyMuPDF first
try:
text, pages = extract_text_with_pymupdf(pdf_bytes)
return text, pages, "text"
# Check if the PDF is scanned (no text content)
if enable_ocr and is_pdf_scanned(text):
logger.info("PDF appears to be scanned, attempting OCR extraction")
try:
ocr_text, ocr_pages = extract_text_with_ocr(pdf_bytes, lang=ocr_lang)
return ocr_text, ocr_pages, "ocr"
except OCRError as ocr_e:
logger.error(f"OCR failed: {str(ocr_e)}")
# Return the minimal text we got, if any
if text.strip():
return text, pages, "pymupdf"
raise PDFExtractionError(f"PDF appears to be scanned and OCR failed: {str(ocr_e)}")
return text, pages, "pymupdf"
except PDFExtractionError as e:
logger.warning(f"PyMuPDF failed, trying pdfplumber: {str(e)}")
# Fallback to pdfplumber
try:
text, pages = extract_text_with_pdfplumber(pdf_bytes)
return text, pages, "text"
# Check if the PDF is scanned
if enable_ocr and is_pdf_scanned(text):
logger.info("PDF appears to be scanned (pdfplumber), attempting OCR extraction")
try:
ocr_text, ocr_pages = extract_text_with_ocr(pdf_bytes, lang=ocr_lang)
return ocr_text, ocr_pages, "ocr"
except OCRError as ocr_e:
logger.error(f"OCR failed: {str(ocr_e)}")
if text.strip():
return text, pages, "pdfplumber"
raise PDFExtractionError(f"PDF appears to be scanned and OCR failed: {str(ocr_e)}")
return text, pages, "pdfplumber"
except PDFExtractionError as e:
logger.error(f"All extraction methods failed: {str(e)}")
raise PDFExtractionError("Failed to extract text from PDF using all available methods")