""" Chandra Document Processing Service Wrapper for Datalab Chandra OCR model """ import logging import os from typing import Optional, Dict, Any from fastapi import FastAPI, HTTPException, File, UploadFile, Form from fastapi.responses import JSONResponse from pydantic import BaseModel import httpx import base64 from io import BytesIO from PIL import Image logger = logging.getLogger(__name__) app = FastAPI(title="Chandra Document Processing Service") # Configuration CHANDRA_API_URL = os.getenv("CHANDRA_API_URL", "http://chandra-inference:8000") CHANDRA_LICENSE_KEY = os.getenv("CHANDRA_LICENSE_KEY", "") CHANDRA_MODEL = os.getenv("CHANDRA_MODEL", "chandra-small") # chandra-small or chandra # Health check endpoint @app.get("/health") async def health(): """Health check endpoint""" try: # Check if Chandra inference service is available async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get(f"{CHANDRA_API_URL}/health") if response.status_code == 200: return { "status": "healthy", "service": "chandra-service", "chandra_api": CHANDRA_API_URL, "model": CHANDRA_MODEL } else: return { "status": "degraded", "service": "chandra-service", "chandra_api": CHANDRA_API_URL, "error": "Chandra inference service unavailable" } except Exception as e: logger.error(f"Health check failed: {e}") return { "status": "unhealthy", "service": "chandra-service", "error": str(e) } class ProcessDocumentRequest(BaseModel): """Request model for document processing""" doc_url: Optional[str] = None doc_base64: Optional[str] = None output_format: str = "markdown" # markdown, html, json accurate_mode: bool = False @app.post("/process") async def process_document( request: ProcessDocumentRequest, file: Optional[UploadFile] = File(None) ): """ Process a document using Chandra OCR. Accepts: - doc_url: URL to document/image - doc_base64: Base64 encoded document/image - file: Uploaded file - output_format: markdown, html, or json - accurate_mode: Use accurate mode (slower but more precise) """ try: # Determine input source image_data = None if file: # Read uploaded file contents = await file.read() image_data = contents elif request.doc_base64: # Decode base64 image_data = base64.b64decode(request.doc_base64) elif request.doc_url: # Download from URL async with httpx.AsyncClient(timeout=30.0) as client: response = await client.get(request.doc_url) if response.status_code == 200: image_data = response.content else: raise HTTPException( status_code=400, detail=f"Failed to download document from URL: {response.status_code}" ) else: raise HTTPException( status_code=400, detail="No document provided. Use file, doc_url, or doc_base64" ) # Prepare request to Chandra inference service files = { "file": ("document", image_data, "application/octet-stream") } data = { "output_format": request.output_format, "accurate_mode": str(request.accurate_mode).lower() } if CHANDRA_LICENSE_KEY: data["license_key"] = CHANDRA_LICENSE_KEY # Call Chandra inference service async with httpx.AsyncClient(timeout=120.0) as client: response = await client.post( f"{CHANDRA_API_URL}/process", files=files, data=data ) if response.status_code == 200: result = response.json() return { "success": True, "output_format": request.output_format, "result": result } else: logger.error(f"Chandra API error: {response.status_code} - {response.text}") raise HTTPException( status_code=response.status_code, detail=f"Chandra API error: {response.text}" ) except httpx.TimeoutException: logger.error("Chandra API timeout") raise HTTPException( status_code=504, detail="Chandra API timeout" ) except Exception as e: logger.error(f"Document processing failed: {e}", exc_info=True) raise HTTPException( status_code=500, detail=f"Document processing failed: {str(e)}" ) @app.get("/models") async def list_models(): """List available Chandra models""" return { "models": [ { "name": "chandra-small", "description": "Fast model with lower latency", "vram_required": "~8GB" }, { "name": "chandra", "description": "Balanced model", "vram_required": "~16GB" } ], "current_model": CHANDRA_MODEL } if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8002)