Spaces:
Sleeping
Sleeping
| import logging | |
| from typing import Any, Dict, Optional | |
| from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile | |
| from fastapi.responses import JSONResponse | |
| # BYOK Integration | |
| 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__) | |
| # Initialize router | |
| router = APIRouter(prefix="/pdf", tags=["PDF Processing"]) | |
| # BYOK Manager dependency | |
| def get_byok_manager_dependency(): | |
| """Get BYOK manager if available""" | |
| if BYOK_AVAILABLE and get_byok_manager: | |
| return get_byok_manager() | |
| return None | |
| # Global service instance (could be dependency injected in production) | |
| _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: | |
| # Initialize with environment variables or defaults | |
| openai_api_key = None # Could be loaded from environment | |
| tesseract_path = None # Could be configured | |
| easyocr_languages = ["en"] # Default to English | |
| _pdf_service = PDFOCRService( | |
| openai_api_key=openai_api_key, | |
| tesseract_path=tesseract_path, | |
| easyocr_languages=easyocr_languages, | |
| use_byok=use_byok, | |
| ) | |
| return _pdf_service | |
| 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()), | |
| } | |
| # Add BYOK integration status if available | |
| 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)}" | |
| ) | |
| 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: | |
| # Validate file type | |
| if not file.filename.lower().endswith(".pdf"): | |
| raise HTTPException(status_code=400, detail="Only PDF files are supported") | |
| # Read file content | |
| file_content = await file.read() | |
| if len(file_content) == 0: | |
| raise HTTPException(status_code=400, detail="Uploaded file is empty") | |
| # Get service instance with BYOK optimization | |
| service = get_pdf_service(use_byok=optimize_with_byok) | |
| # Add BYOK optimization info if available | |
| 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 | |
| ) | |
| } | |
| # Process the PDF | |
| 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, | |
| ) | |
| # Add file metadata to result | |
| result["file_metadata"] = { | |
| "filename": file.filename, | |
| "size_bytes": len(file_content), | |
| "content_type": file.content_type, | |
| } | |
| # Add BYOK optimization info to result | |
| 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)}") | |
| 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 | |
| # Download PDF from URL | |
| 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 | |
| # Get service instance with BYOK optimization | |
| service = get_pdf_service(use_byok=optimize_with_byok) | |
| # Add BYOK optimization info if available | |
| 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 | |
| ) | |
| } | |
| # Process the PDF | |
| result = await service.process_pdf( | |
| pdf_data=pdf_content, | |
| use_ocr=use_ocr, | |
| extract_images=extract_images, | |
| use_advanced_comprehension=use_advanced_comprehension, | |
| ) | |
| # Add URL metadata to result | |
| result["source_metadata"] = {"url": pdf_url, "size_bytes": len(pdf_content)} | |
| # Add BYOK optimization info to result | |
| 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)}") | |
| 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: | |
| # Validate file type | |
| if not file.filename.lower().endswith(".pdf"): | |
| raise HTTPException(status_code=400, detail="Only PDF files are supported") | |
| # Read file content | |
| file_content = await file.read() | |
| # Get service instance | |
| service = get_pdf_service() | |
| # Process with OCR disabled | |
| result = await service.process_pdf( | |
| pdf_data=file_content, | |
| use_ocr=False, | |
| extract_images=False, | |
| use_advanced_comprehension=False, | |
| ) | |
| # Return simplified response | |
| 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)}") | |
| async def analyze_pdf_type( | |
| file: UploadFile = File(..., description="PDF file to analyze"), | |
| ): | |
| """ | |
| Analyze PDF type (searchable vs scanned) without full processing. | |
| """ | |
| try: | |
| # Validate file type | |
| if not file.filename.lower().endswith(".pdf"): | |
| raise HTTPException(status_code=400, detail="Only PDF files are supported") | |
| # Read file content | |
| file_content = await file.read() | |
| # Get service instance | |
| service = get_pdf_service() | |
| # Just run basic extraction to get text ratio | |
| basic_result = await service._extract_basic_text(file_content) | |
| # Determine PDF type based on text ratio | |
| 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)}") | |
| async def health_check(byok_manager=Depends(get_byok_manager_dependency)): | |
| """Health check endpoint for PDF processing service.""" | |
| try: | |
| service = get_pdf_service() | |
| # Test basic functionality | |
| 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()), | |
| } | |
| # Add BYOK health info if available | |
| 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)}") | |
| # BYOK Integration Helper Functions | |
| 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: | |
| # Determine task type based on processing requirements | |
| if use_advanced_comprehension: | |
| task_type = "image_comprehension" | |
| elif use_ocr: | |
| task_type = "pdf_ocr" | |
| else: | |
| task_type = "document_processing" | |
| # Get optimal provider | |
| 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"} | |