281 lines
9.0 KiB
Python
281 lines
9.0 KiB
Python
import os
|
|
import tempfile
|
|
import logging
|
|
import asyncio
|
|
from typing import List, Optional
|
|
from pathlib import Path
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
import time
|
|
|
|
from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel
|
|
import pytesseract
|
|
from PIL import Image
|
|
from pdf2image import convert_from_path
|
|
import uvicorn
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app = FastAPI(
|
|
title="OCR API",
|
|
description="API for extracting text from PDF files using OCR",
|
|
version="1.0.0"
|
|
)
|
|
|
|
class OCRResponse(BaseModel):
|
|
text: str
|
|
pages: int
|
|
processing_time: float
|
|
confidence: Optional[float] = None
|
|
|
|
class ErrorResponse(BaseModel):
|
|
error: str
|
|
detail: str
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
return {"message": "OCR API is running. Use /docs for API documentation."}
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
return {"status": "healthy", "service": "OCR API"}
|
|
|
|
def process_single_page(args):
|
|
"""
|
|
Process a single page with OCR (runs in thread pool)
|
|
"""
|
|
page_num, image = args
|
|
try:
|
|
logger.info(f"Processing page {page_num + 1}")
|
|
|
|
# Perform OCR on the image
|
|
ocr_data = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT)
|
|
|
|
# Extract text and confidence scores
|
|
page_text = []
|
|
total_confidence = 0
|
|
confidence_count = 0
|
|
|
|
for j in range(len(ocr_data['text'])):
|
|
if int(ocr_data['conf'][j]) > 0: # Filter out low confidence results
|
|
page_text.append(ocr_data['text'][j])
|
|
total_confidence += float(ocr_data['conf'][j])
|
|
confidence_count += 1
|
|
|
|
page_text = ' '.join(page_text).strip()
|
|
avg_confidence = total_confidence / confidence_count if confidence_count > 0 else 0
|
|
|
|
return {
|
|
'page_num': page_num,
|
|
'text': page_text,
|
|
'confidence': avg_confidence,
|
|
'success': True
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error processing page {page_num + 1}: {str(e)}")
|
|
return {
|
|
'page_num': page_num,
|
|
'text': '',
|
|
'confidence': 0,
|
|
'success': False,
|
|
'error': str(e)
|
|
}
|
|
|
|
async def process_pdf_with_ocr(pdf_path: str, max_workers: int = None) -> OCRResponse:
|
|
"""
|
|
Process a PDF file and extract text using OCR with concurrent page processing
|
|
"""
|
|
start_time = time.time()
|
|
|
|
try:
|
|
# Convert PDF to images
|
|
logger.info(f"Converting PDF to images: {pdf_path}")
|
|
images = convert_from_path(pdf_path, dpi=300)
|
|
|
|
if not images:
|
|
raise HTTPException(status_code=400, detail="Could not extract images from PDF")
|
|
|
|
# Determine optimal number of workers
|
|
if max_workers is None:
|
|
max_workers = min(len(images), os.cpu_count() or 4)
|
|
|
|
logger.info(f"Processing {len(images)} pages with {max_workers} workers")
|
|
|
|
# Process pages concurrently using ThreadPoolExecutor
|
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
# Create tasks for each page
|
|
page_tasks = [(i, image) for i, image in enumerate(images)]
|
|
|
|
# Submit all tasks and wait for completion
|
|
results = list(executor.map(process_single_page, page_tasks))
|
|
|
|
# Sort results by page number and combine
|
|
results.sort(key=lambda x: x['page_num'])
|
|
|
|
all_text = []
|
|
total_confidence = 0
|
|
confidence_count = 0
|
|
successful_pages = 0
|
|
|
|
for result in results:
|
|
if result['success']:
|
|
successful_pages += 1
|
|
if result['text']:
|
|
all_text.append(f"--- Page {result['page_num'] + 1} ---\n{result['text']}")
|
|
total_confidence += result['confidence']
|
|
confidence_count += 1
|
|
else:
|
|
logger.warning(f"Page {result['page_num'] + 1} failed: {result.get('error', 'Unknown error')}")
|
|
|
|
# Calculate average confidence
|
|
avg_confidence = total_confidence / confidence_count if confidence_count > 0 else None
|
|
|
|
processing_time = time.time() - start_time
|
|
|
|
logger.info(f"Completed processing {successful_pages}/{len(images)} pages in {processing_time:.2f}s")
|
|
|
|
return OCRResponse(
|
|
text='\n\n'.join(all_text),
|
|
pages=len(images),
|
|
processing_time=round(processing_time, 2),
|
|
confidence=round(avg_confidence, 2) if avg_confidence else None
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing PDF: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=f"Error processing PDF: {str(e)}")
|
|
|
|
@app.post("/ocr/pdf", response_model=OCRResponse)
|
|
async def extract_text_from_pdf(
|
|
file: UploadFile = File(...),
|
|
max_workers: int = None,
|
|
background_tasks: BackgroundTasks = None
|
|
):
|
|
"""
|
|
Extract text from a PDF file using OCR with concurrent processing
|
|
|
|
- **file**: PDF file to process
|
|
- **max_workers**: Maximum number of concurrent workers (default: min(pages, CPU cores))
|
|
- Returns extracted text with metadata
|
|
"""
|
|
|
|
# Validate file type
|
|
if not file.filename.lower().endswith('.pdf'):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="File must be a PDF"
|
|
)
|
|
|
|
# Check file size (limit to 50MB)
|
|
if file.size and file.size > 50 * 1024 * 1024:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="File size too large. Maximum size is 50MB"
|
|
)
|
|
|
|
try:
|
|
# Create temporary file
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
|
|
# Write uploaded file to temporary file
|
|
content = await file.read()
|
|
temp_file.write(content)
|
|
temp_file_path = temp_file.name
|
|
|
|
try:
|
|
# Process the PDF
|
|
result = await process_pdf_with_ocr(temp_file_path, max_workers)
|
|
|
|
# Clean up temporary file
|
|
background_tasks.add_task(os.unlink, temp_file_path)
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
# Clean up temporary file on error
|
|
if os.path.exists(temp_file_path):
|
|
os.unlink(temp_file_path)
|
|
raise e
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error handling file upload: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=f"Error processing file: {str(e)}")
|
|
|
|
@app.post("/ocr/image", response_model=OCRResponse)
|
|
async def extract_text_from_image(
|
|
file: UploadFile = File(...),
|
|
background_tasks: BackgroundTasks = None
|
|
):
|
|
"""
|
|
Extract text from an image file using OCR
|
|
|
|
- **file**: Image file to process (PNG, JPG, JPEG, etc.)
|
|
- Returns extracted text with metadata
|
|
"""
|
|
|
|
# Validate file type
|
|
allowed_extensions = {'.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.tif'}
|
|
file_extension = Path(file.filename).suffix.lower()
|
|
|
|
if file_extension not in allowed_extensions:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Unsupported file type. Allowed types: {', '.join(allowed_extensions)}"
|
|
)
|
|
|
|
try:
|
|
# Create temporary file
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file:
|
|
content = await file.read()
|
|
temp_file.write(content)
|
|
temp_file_path = temp_file.name
|
|
|
|
try:
|
|
import time
|
|
start_time = time.time()
|
|
|
|
# Open and process image
|
|
image = Image.open(temp_file_path)
|
|
|
|
# Perform OCR
|
|
ocr_data = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT)
|
|
|
|
# Extract text and confidence
|
|
text_parts = []
|
|
total_confidence = 0
|
|
confidence_count = 0
|
|
|
|
for i in range(len(ocr_data['text'])):
|
|
if int(ocr_data['conf'][i]) > 0:
|
|
text_parts.append(ocr_data['text'][i])
|
|
total_confidence += float(ocr_data['conf'][i])
|
|
confidence_count += 1
|
|
|
|
text = ' '.join(text_parts).strip()
|
|
avg_confidence = total_confidence / confidence_count if confidence_count > 0 else None
|
|
processing_time = time.time() - start_time
|
|
|
|
# Clean up temporary file
|
|
background_tasks.add_task(os.unlink, temp_file_path)
|
|
|
|
return OCRResponse(
|
|
text=text,
|
|
pages=1,
|
|
processing_time=round(processing_time, 2),
|
|
confidence=round(avg_confidence, 2) if avg_confidence else None
|
|
)
|
|
|
|
except Exception as e:
|
|
# Clean up temporary file on error
|
|
if os.path.exists(temp_file_path):
|
|
os.unlink(temp_file_path)
|
|
raise e
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing image: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run(app, host="0.0.0.0", port=8000) |