| import logging |
| from typing import Any, Dict, Optional |
| from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile |
| from fastapi.responses import JSONResponse |
|
|
| |
| try: |
| from backend.core.byok_endpoints import get_byok_manager |
|
|
| BYOK_AVAILABLE = True |
| except ImportError: |
| BYOK_AVAILABLE = False |
| get_byok_manager = None |
|
|
| from .pdf_ocr_service import PDFOCRService |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| router = APIRouter(prefix="/pdf", tags=["PDF Processing"]) |
|
|
|
|
| |
| def get_byok_manager_dependency(): |
| """Get BYOK manager if available""" |
| if BYOK_AVAILABLE and get_byok_manager: |
| return get_byok_manager() |
| return None |
|
|
|
|
| |
| _pdf_service: Optional[PDFOCRService] = None |
|
|
|
|
| def get_pdf_service(use_byok: bool = True) -> PDFOCRService: |
| """Get or initialize the PDF OCR service.""" |
| global _pdf_service |
| if _pdf_service is None: |
| |
| openai_api_key = None |
| tesseract_path = None |
| easyocr_languages = ["en"] |
|
|
| _pdf_service = PDFOCRService( |
| openai_api_key=openai_api_key, |
| tesseract_path=tesseract_path, |
| easyocr_languages=easyocr_languages, |
| use_byok=use_byok, |
| ) |
| return _pdf_service |
|
|
|
|
| @router.get("/status") |
| async def get_pdf_service_status(byok_manager=Depends(get_byok_manager_dependency)): |
| """Get the status and capabilities of the PDF processing service.""" |
| try: |
| service = get_pdf_service() |
| status_info = { |
| "status": "available", |
| "service_capabilities": service.service_status, |
| "available_ocr_methods": list(service.ocr_readers.keys()), |
| } |
|
|
| |
| if BYOK_AVAILABLE and byok_manager: |
| byok_status = { |
| "byok_integrated": True, |
| "byok_manager_available": byok_manager is not None, |
| "pdf_providers": await _get_pdf_byok_providers(byok_manager), |
| } |
| status_info["byok_integration"] = byok_status |
|
|
| return status_info |
| except Exception as e: |
| logger.error(f"Failed to get PDF service status: {e}") |
| raise HTTPException( |
| status_code=500, detail=f"Service status check failed: {str(e)}" |
| ) |
|
|
|
|
| @router.post("/process") |
| async def process_pdf_file( |
| file: UploadFile = File(..., description="PDF file to process"), |
| use_ocr: bool = Form(True, description="Use OCR for scanned PDFs"), |
| extract_images: bool = Form(True, description="Extract and process images"), |
| use_advanced_comprehension: bool = Form( |
| False, description="Use AI for image understanding" |
| ), |
| fallback_strategy: str = Form( |
| "cascade", description="OCR fallback strategy: cascade or parallel" |
| ), |
| optimize_with_byok: bool = Form( |
| True, description="Use BYOK for provider optimization" |
| ), |
| byok_manager=Depends(get_byok_manager_dependency), |
| ): |
| """ |
| Process a PDF file with optional OCR and image comprehension. |
| |
| This endpoint supports both searchable PDFs (text extraction) and scanned PDFs (OCR). |
| """ |
| try: |
| |
| if not file.filename.lower().endswith(".pdf"): |
| raise HTTPException(status_code=400, detail="Only PDF files are supported") |
|
|
| |
| file_content = await file.read() |
| if len(file_content) == 0: |
| raise HTTPException(status_code=400, detail="Uploaded file is empty") |
|
|
| |
| service = get_pdf_service(use_byok=optimize_with_byok) |
|
|
| |
| optimization_info = {} |
| if optimize_with_byok and byok_manager: |
| optimization_info = { |
| "byok_optimization": await _optimize_pdf_processing_with_byok( |
| byok_manager, use_advanced_comprehension, use_ocr |
| ) |
| } |
|
|
| |
| result = await service.process_pdf( |
| pdf_data=file_content, |
| use_ocr=use_ocr, |
| extract_images=extract_images, |
| use_advanced_comprehension=use_advanced_comprehension, |
| fallback_strategy=fallback_strategy, |
| ) |
|
|
| |
| result["file_metadata"] = { |
| "filename": file.filename, |
| "size_bytes": len(file_content), |
| "content_type": file.content_type, |
| } |
|
|
| |
| if optimization_info: |
| result["byok_optimization"] = optimization_info["byok_optimization"] |
|
|
| return JSONResponse(content=result) |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"PDF processing failed: {e}") |
| raise HTTPException(status_code=500, detail=f"PDF processing failed: {str(e)}") |
|
|
|
|
| @router.post("/process-url") |
| async def process_pdf_from_url( |
| pdf_url: str = Form(..., description="URL of PDF to process"), |
| use_ocr: bool = Form(True, description="Use OCR for scanned PDFs"), |
| extract_images: bool = Form(True, description="Extract and process images"), |
| use_advanced_comprehension: bool = Form( |
| False, description="Use AI for image understanding" |
| ), |
| optimize_with_byok: bool = Form( |
| True, description="Use BYOK for provider optimization" |
| ), |
| byok_manager=Depends(get_byok_manager_dependency), |
| ): |
| """ |
| Process a PDF from a URL with optional OCR and image comprehension. |
| """ |
| try: |
| import httpx |
|
|
| |
| async with httpx.AsyncClient() as client: |
| response = await client.get(pdf_url) |
| response.raise_for_status() |
|
|
| if ( |
| not response.headers.get("content-type", "") |
| .lower() |
| .startswith("application/pdf") |
| ): |
| raise HTTPException( |
| status_code=400, detail="URL does not point to a PDF file" |
| ) |
|
|
| pdf_content = response.content |
|
|
| |
| service = get_pdf_service(use_byok=optimize_with_byok) |
|
|
| |
| optimization_info = {} |
| if optimize_with_byok and byok_manager: |
| optimization_info = { |
| "byok_optimization": await _optimize_pdf_processing_with_byok( |
| byok_manager, use_advanced_comprehension, use_ocr |
| ) |
| } |
|
|
| |
| result = await service.process_pdf( |
| pdf_data=pdf_content, |
| use_ocr=use_ocr, |
| extract_images=extract_images, |
| use_advanced_comprehension=use_advanced_comprehension, |
| ) |
|
|
| |
| result["source_metadata"] = {"url": pdf_url, "size_bytes": len(pdf_content)} |
|
|
| |
| if optimization_info: |
| result["byok_optimization"] = optimization_info["byok_optimization"] |
|
|
| return JSONResponse(content=result) |
|
|
| except httpx.HTTPError as e: |
| logger.error(f"Failed to download PDF from URL: {e}") |
| raise HTTPException( |
| status_code=400, detail=f"Failed to download PDF from URL: {str(e)}" |
| ) |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"PDF processing from URL failed: {e}") |
| raise HTTPException(status_code=500, detail=f"PDF processing failed: {str(e)}") |
|
|
|
|
| @router.post("/extract-text-only") |
| async def extract_text_only( |
| file: UploadFile = File(..., description="PDF file for text extraction"), |
| ): |
| """ |
| Extract text only from PDF (no OCR, no image processing). |
| Fast processing for searchable PDFs. |
| """ |
| try: |
| |
| if not file.filename.lower().endswith(".pdf"): |
| raise HTTPException(status_code=400, detail="Only PDF files are supported") |
|
|
| |
| file_content = await file.read() |
|
|
| |
| service = get_pdf_service() |
|
|
| |
| result = await service.process_pdf( |
| pdf_data=file_content, |
| use_ocr=False, |
| extract_images=False, |
| use_advanced_comprehension=False, |
| ) |
|
|
| |
| simplified_result = { |
| "extracted_text": result.get("extracted_content", {}).get("text", ""), |
| "page_count": result.get("processing_summary", {}).get("total_pages", 0), |
| "total_characters": result.get("processing_summary", {}).get( |
| "total_characters", 0 |
| ), |
| "method_used": result.get("processing_summary", {}).get( |
| "best_method", "unknown" |
| ), |
| "filename": file.filename, |
| } |
|
|
| return JSONResponse(content=simplified_result) |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Text extraction failed: {e}") |
| raise HTTPException(status_code=500, detail=f"Text extraction failed: {str(e)}") |
|
|
|
|
| @router.post("/analyze-pdf-type") |
| async def analyze_pdf_type( |
| file: UploadFile = File(..., description="PDF file to analyze"), |
| ): |
| """ |
| Analyze PDF type (searchable vs scanned) without full processing. |
| """ |
| try: |
| |
| if not file.filename.lower().endswith(".pdf"): |
| raise HTTPException(status_code=400, detail="Only PDF files are supported") |
|
|
| |
| file_content = await file.read() |
|
|
| |
| service = get_pdf_service() |
|
|
| |
| basic_result = await service._extract_basic_text(file_content) |
|
|
| |
| text_ratio = basic_result.get("text_ratio", 0) |
| if text_ratio > 0.5: |
| pdf_type = "searchable" |
| confidence = "high" |
| elif text_ratio > 0.1: |
| pdf_type = "mostly_searchable" |
| confidence = "medium" |
| else: |
| pdf_type = "scanned_or_image_based" |
| confidence = "high" |
|
|
| analysis_result = { |
| "pdf_type": pdf_type, |
| "confidence": confidence, |
| "text_ratio": text_ratio, |
| "total_pages": basic_result.get("page_count", 0), |
| "total_characters": basic_result.get("total_chars", 0), |
| "filename": file.filename, |
| "recommended_processing": { |
| "needs_ocr": pdf_type |
| in ["scanned_or_image_based", "mostly_searchable"], |
| "suggested_methods": ["basic_extraction"] |
| if pdf_type == "searchable" |
| else ["ocr_processing"], |
| }, |
| } |
|
|
| return JSONResponse(content=analysis_result) |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"PDF analysis failed: {e}") |
| raise HTTPException(status_code=500, detail=f"PDF analysis failed: {str(e)}") |
|
|
|
|
| @router.get("/health") |
| async def health_check(byok_manager=Depends(get_byok_manager_dependency)): |
| """Health check endpoint for PDF processing service.""" |
| try: |
| service = get_pdf_service() |
|
|
| |
| test_pdf = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>\nendobj\n4 0 obj\n<< /Length 44 >>\nstream\nBT\n/F1 12 Tf\n72 720 Td\n(Test PDF) Tj\nET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000204 00000 n \ntrailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n281\n%%EOF" |
|
|
| result = await service.process_pdf( |
| pdf_data=test_pdf, use_ocr=False, extract_images=False |
| ) |
|
|
| health_info = { |
| "status": "healthy", |
| "service": "PDF OCR Processing", |
| "basic_functionality": result.get("success", False), |
| "available_methods": list(service.ocr_readers.keys()), |
| } |
|
|
| |
| if BYOK_AVAILABLE and byok_manager: |
| try: |
| byok_health = await byok_manager.get("/api/ai/health") |
| health_info["byok_integration"] = { |
| "status": "connected", |
| "byok_health": byok_health, |
| } |
| except Exception as e: |
| health_info["byok_integration"] = { |
| "status": "disconnected", |
| "error": str(e), |
| } |
|
|
| return health_info |
|
|
| except Exception as e: |
| logger.error(f"Health check failed: {e}") |
| raise HTTPException(status_code=503, detail=f"Service unhealthy: {str(e)}") |
|
|
|
|
| |
|
|
|
|
| async def _get_pdf_byok_providers(byok_manager) -> Dict[str, Any]: |
| """Get PDF-specific BYOK providers.""" |
| try: |
| pdf_providers = [] |
| for provider_id in ["openai", "google_gemini", "anthropic", "azure_openai"]: |
| try: |
| provider_status = byok_manager.get_provider_status(provider_id) |
| if provider_status["status"] == "active": |
| pdf_providers.append( |
| { |
| "provider_id": provider_id, |
| "name": provider_status["provider"]["name"], |
| "supported_tasks": provider_status["provider"][ |
| "supported_tasks" |
| ], |
| "cost_per_token": provider_status["provider"][ |
| "cost_per_token" |
| ], |
| } |
| ) |
| except Exception: |
| continue |
|
|
| return {"pdf_providers": pdf_providers, "total_providers": len(pdf_providers)} |
| except Exception as e: |
| logger.error(f"Failed to get PDF BYOK providers: {e}") |
| return {"pdf_providers": [], "total_providers": 0, "error": str(e)} |
|
|
|
|
| async def _optimize_pdf_processing_with_byok( |
| byok_manager, use_advanced_comprehension: bool, use_ocr: bool |
| ) -> Dict[str, Any]: |
| """Optimize PDF processing using BYOK system.""" |
| try: |
| |
| if use_advanced_comprehension: |
| task_type = "image_comprehension" |
| elif use_ocr: |
| task_type = "pdf_ocr" |
| else: |
| task_type = "document_processing" |
|
|
| |
| optimal_provider = byok_manager.get_optimal_provider(task_type) |
|
|
| if optimal_provider: |
| provider_status = byok_manager.get_provider_status(optimal_provider) |
| return { |
| "optimized": True, |
| "task_type": task_type, |
| "optimal_provider": optimal_provider, |
| "provider_name": provider_status["provider"]["name"], |
| "cost_per_token": provider_status["provider"]["cost_per_token"], |
| "supported_tasks": provider_status["provider"]["supported_tasks"], |
| } |
| else: |
| return { |
| "optimized": False, |
| "reason": f"No suitable providers found for {task_type}", |
| "task_type": task_type, |
| } |
|
|
| except Exception as e: |
| logger.error(f"BYOK optimization failed: {e}") |
| return {"optimized": False, "error": str(e), "task_type": "unknown"} |
|
|