Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, File, UploadFile, HTTPException | |
| from fastapi.responses import JSONResponse, HTMLResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| import base64 | |
| import io | |
| from PIL import Image | |
| from lab_analyzer import LabReportAnalyzer | |
| import logging | |
| import os | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # Initialize FastAPI app | |
| app = FastAPI( | |
| title="Lab Report Analysis API", | |
| description="AI-powered lab report analysis service using Google AI Studio", | |
| version="1.0.0", | |
| docs_url="/docs", | |
| redoc_url="/redoc" | |
| ) | |
| # Add CORS middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # Configure this properly for production | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Initialize the lab analyzer | |
| analyzer = LabReportAnalyzer() | |
| async def health_check(): | |
| """Health check endpoint for monitoring""" | |
| return {"status": "healthy", "service": "lab-report-analyzer"} | |
| async def analyze_lab_report(file: UploadFile = File(...)): | |
| """ | |
| Analyze a lab report image and return structured results | |
| Args: | |
| file: Uploaded image file (jpg, jpeg, png, bmp, tiff, webp) | |
| Returns: | |
| JSON response with analysis results | |
| """ | |
| try: | |
| # Validate file type | |
| if not file.content_type.startswith('image/'): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="File must be an image (jpg, jpeg, png, bmp, tiff, webp)" | |
| ) | |
| # Read and validate image | |
| contents = await file.read() | |
| if len(contents) == 0: | |
| raise HTTPException(status_code=400, detail="Empty file uploaded") | |
| # Validate image can be opened | |
| try: | |
| image = Image.open(io.BytesIO(contents)) | |
| image.verify() # Verify it's a valid image | |
| except Exception as e: | |
| raise HTTPException(status_code=400, detail=f"Invalid image file: {str(e)}") | |
| # Convert to base64 for analysis | |
| image_b64 = base64.b64encode(contents).decode("utf-8") | |
| # Analyze the lab report | |
| logger.info(f"Analyzing lab report: {file.filename}") | |
| analysis_result = await analyzer.analyze_report(image_b64) | |
| return JSONResponse( | |
| status_code=200, | |
| content={ | |
| "success": True, | |
| "filename": file.filename, | |
| "analysis": analysis_result | |
| } | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error analyzing lab report: {str(e)}") | |
| raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") | |
| async def analyze_lab_report_base64(data: dict): | |
| """ | |
| Analyze a lab report from base64 encoded image | |
| Args: | |
| data: JSON with 'image' key containing base64 encoded image | |
| Returns: | |
| JSON response with analysis results | |
| """ | |
| try: | |
| if 'image' not in data: | |
| raise HTTPException(status_code=400, detail="Missing 'image' field in request body") | |
| image_b64 = data['image'] | |
| # Remove data:image/...;base64, prefix if present | |
| if image_b64.startswith('data:image'): | |
| image_b64 = image_b64.split(',')[1] | |
| # Validate base64 and image | |
| try: | |
| image_bytes = base64.b64decode(image_b64) | |
| image = Image.open(io.BytesIO(image_bytes)) | |
| image.verify() | |
| except Exception as e: | |
| raise HTTPException(status_code=400, detail=f"Invalid base64 image: {str(e)}") | |
| # Analyze the lab report | |
| logger.info("Analyzing lab report from base64 data") | |
| analysis_result = await analyzer.analyze_report(image_b64) | |
| return JSONResponse( | |
| status_code=200, | |
| content={ | |
| "success": True, | |
| "analysis": analysis_result | |
| } | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error analyzing base64 lab report: {str(e)}") | |
| raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") | |
| # Flutter-friendly endpoint aliases | |
| async def analyze_lab_api(file: UploadFile = File(...)): | |
| """Flutter-friendly endpoint for lab analysis""" | |
| return await analyze_lab_report(file) | |
| async def analyze_lab_base64_api(data: dict): | |
| """Flutter-friendly endpoint for base64 lab analysis""" | |
| return await analyze_lab_report_base64(data) | |
| async def root(): | |
| """Main page with upload interface""" | |
| return """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>π₯ Lab Report Analysis AI</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 20px; | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.1); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(45deg, #2196F3, #21CBF3); | |
| color: white; | |
| padding: 40px; | |
| text-align: center; | |
| } | |
| .header h1 { font-size: 2.5em; margin-bottom: 10px; } | |
| .header p { font-size: 1.2em; opacity: 0.9; } | |
| .content { padding: 40px; } | |
| .upload-area { | |
| border: 3px dashed #2196F3; | |
| border-radius: 15px; | |
| padding: 40px; | |
| text-align: center; | |
| margin: 20px 0; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| } | |
| .upload-area:hover { | |
| border-color: #1976D2; | |
| background-color: #f5f5f5; | |
| } | |
| .upload-area.dragover { | |
| border-color: #4CAF50; | |
| background-color: #e8f5e8; | |
| } | |
| input[type="file"] { display: none; } | |
| .btn { | |
| background: linear-gradient(45deg, #2196F3, #21CBF3); | |
| color: white; | |
| border: none; | |
| padding: 15px 30px; | |
| border-radius: 25px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| margin: 10px; | |
| transition: transform 0.2s ease; | |
| } | |
| .btn:hover { transform: translateY(-2px); } | |
| .btn:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .result { | |
| margin-top: 30px; | |
| padding: 20px; | |
| border-radius: 10px; | |
| background: #f8f9fa; | |
| border-left: 5px solid #2196F3; | |
| } | |
| .loading { | |
| text-align: center; | |
| color: #2196F3; | |
| font-size: 18px; | |
| } | |
| .error { | |
| background: #ffebee; | |
| border-left-color: #f44336; | |
| color: #c62828; | |
| } | |
| .success { | |
| background: #e8f5e8; | |
| border-left-color: #4CAF50; | |
| } | |
| .api-info { | |
| background: #f0f4f8; | |
| padding: 30px; | |
| margin-top: 40px; | |
| border-radius: 15px; | |
| } | |
| .endpoint { | |
| background: #fff; | |
| padding: 15px; | |
| margin: 10px 0; | |
| border-radius: 8px; | |
| border-left: 4px solid #2196F3; | |
| } | |
| .method { | |
| background: #2196F3; | |
| color: white; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-weight: bold; | |
| font-size: 12px; | |
| } | |
| pre { | |
| background: #2d3748; | |
| color: #e2e8f0; | |
| padding: 15px; | |
| border-radius: 8px; | |
| overflow-x: auto; | |
| margin: 10px 0; | |
| } | |
| .features { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 20px; | |
| margin: 30px 0; | |
| } | |
| .feature { | |
| text-align: center; | |
| padding: 20px; | |
| border-radius: 10px; | |
| background: #f8f9fa; | |
| } | |
| .feature-icon { | |
| font-size: 2em; | |
| margin-bottom: 10px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>π₯ Lab Report Analysis AI</h1> | |
| <p>AI-powered medical lab report analysis using Google AI Studio</p> | |
| </div> | |
| <div class="content"> | |
| <div class="features"> | |
| <div class="feature"> | |
| <div class="feature-icon">πΈ</div> | |
| <h3>Image Upload</h3> | |
| <p>Support for JPG, PNG, TIFF, and more</p> | |
| </div> | |
| <div class="feature"> | |
| <div class="feature-icon">π€</div> | |
| <h3>AI Analysis</h3> | |
| <p>Google Gemini 2.0 Flash model</p> | |
| </div> | |
| <div class="feature"> | |
| <div class="feature-icon">π</div> | |
| <h3>Structured Results</h3> | |
| <p>Summary, findings, and interpretations</p> | |
| </div> | |
| <div class="feature"> | |
| <div class="feature-icon">β‘</div> | |
| <h3>Fast Processing</h3> | |
| <p>Real-time analysis results</p> | |
| </div> | |
| </div> | |
| <div class="upload-area" onclick="document.getElementById('fileInput').click()"> | |
| <h3>π€ Upload Lab Report Image</h3> | |
| <p>Click here or drag and drop your lab report image</p> | |
| <input type="file" id="fileInput" accept="image/*"> | |
| </div> | |
| <div style="text-align: center;"> | |
| <button class="btn" onclick="analyzeImage()" id="analyzeBtn" disabled> | |
| π¬ Analyze Report | |
| </button> | |
| <button class="btn" onclick="clearResults()"> | |
| ποΈ Clear | |
| </button> | |
| </div> | |
| <div id="result"></div> | |
| <div class="api-info"> | |
| <h2>π§ API Documentation</h2> | |
| <p>This service provides RESTful API endpoints for programmatic access:</p> | |
| <div class="endpoint"> | |
| <span class="method">POST</span> <strong>/analyze</strong> | |
| <p>Upload lab report image file for analysis</p> | |
| </div> | |
| <div class="endpoint"> | |
| <span class="method">POST</span> <strong>/analyze-base64</strong> | |
| <p>Analyze lab report from base64 encoded image</p> | |
| </div> | |
| <div class="endpoint"> | |
| <span class="method">POST</span> <strong>/api/analyze-lab</strong> | |
| <p>Flutter-friendly endpoint for file upload</p> | |
| </div> | |
| <div class="endpoint"> | |
| <span class="method">POST</span> <strong>/api/analyze-lab-base64</strong> | |
| <p>Flutter-friendly endpoint for base64 analysis</p> | |
| </div> | |
| <p><strong>π Interactive Documentation:</strong></p> | |
| <button class="btn" onclick="window.open('/docs', '_blank')"> | |
| π OpenAPI Docs | |
| </button> | |
| <button class="btn" onclick="window.open('/redoc', '_blank')"> | |
| π ReDoc | |
| </button> | |
| </div> | |
| <div style="text-align: center; margin-top: 40px; color: #666;"> | |
| <p>β οΈ This tool is for educational purposes only. Always consult healthcare professionals for medical advice.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let selectedFile = null; | |
| document.getElementById('fileInput').addEventListener('change', function(e) { | |
| selectedFile = e.target.files[0]; | |
| if (selectedFile) { | |
| document.querySelector('.upload-area h3').innerHTML = 'β ' + selectedFile.name; | |
| document.getElementById('analyzeBtn').disabled = false; | |
| } | |
| }); | |
| // Drag and drop functionality | |
| const uploadArea = document.querySelector('.upload-area'); | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.add('dragover'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => { | |
| uploadArea.classList.remove('dragover'); | |
| }); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('dragover'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| selectedFile = files[0]; | |
| document.querySelector('.upload-area h3').innerHTML = 'β ' + selectedFile.name; | |
| document.getElementById('analyzeBtn').disabled = false; | |
| } | |
| }); | |
| async function analyzeImage() { | |
| if (!selectedFile) { | |
| alert('Please select an image first'); | |
| return; | |
| } | |
| const resultDiv = document.getElementById('result'); | |
| resultDiv.innerHTML = '<div class="result loading">π Analyzing lab report...</div>'; | |
| const analyzeBtn = document.getElementById('analyzeBtn'); | |
| analyzeBtn.disabled = true; | |
| analyzeBtn.innerHTML = 'β³ Processing...'; | |
| const formData = new FormData(); | |
| formData.append('file', selectedFile); | |
| try { | |
| const response = await fetch('/analyze', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| const analysis = data.analysis; | |
| resultDiv.innerHTML = ` | |
| <div class="result success"> | |
| <h3>π Analysis Results</h3> | |
| <h4>π Summary</h4> | |
| <p>${analysis.summary || 'No summary available'}</p> | |
| <h4>π Key Findings</h4> | |
| <ul> | |
| ${analysis.key_findings?.map(finding => `<li>${finding}</li>`).join('') || '<li>No specific findings identified</li>'} | |
| </ul> | |
| <h4>π‘ Interpretation</h4> | |
| <p>${analysis.interpretation || 'No interpretation available'}</p> | |
| <h4>β οΈ Important Note</h4> | |
| <p><em>${analysis.note || 'This analysis is for educational purposes only and should not replace professional medical advice.'}</em></p> | |
| </div> | |
| `; | |
| } else { | |
| resultDiv.innerHTML = `<div class="result error">β Error: ${data.error || 'Analysis failed'}</div>`; | |
| } | |
| } catch (error) { | |
| resultDiv.innerHTML = `<div class="result error">β Error: ${error.message}</div>`; | |
| } finally { | |
| analyzeBtn.disabled = false; | |
| analyzeBtn.innerHTML = 'π¬ Analyze Report'; | |
| } | |
| } | |
| function clearResults() { | |
| document.getElementById('result').innerHTML = ''; | |
| document.getElementById('fileInput').value = ''; | |
| document.querySelector('.upload-area h3').innerHTML = 'π€ Upload Lab Report Image'; | |
| document.getElementById('analyzeBtn').disabled = true; | |
| selectedFile = null; | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True) |