| """ |
| Analysis Routes - Image analysis with SSE streaming |
| """ |
|
|
| from fastapi import APIRouter, Query, HTTPException |
| from fastapi.responses import StreamingResponse, FileResponse |
| from pathlib import Path |
| import json |
| import tempfile |
|
|
| from backend.services.analysis_service import get_analysis_service |
| from data.case_store import get_case_store |
|
|
| router = APIRouter() |
|
|
|
|
| @router.get("/gradcam") |
| def get_gradcam_by_path(path: str = Query(...)): |
| """Serve a temp visualization image (GradCAM or comparison overlay)""" |
| if not path: |
| raise HTTPException(status_code=400, detail="No path provided") |
|
|
| temp_dir = Path(tempfile.gettempdir()).resolve() |
| resolved_path = Path(path).resolve() |
| if not str(resolved_path).startswith(str(temp_dir)): |
| raise HTTPException(status_code=403, detail="Access denied") |
|
|
| allowed_suffixes = ("_gradcam.png", "_comparison.png") |
| if not any(resolved_path.name.endswith(s) for s in allowed_suffixes): |
| raise HTTPException(status_code=400, detail="Invalid image path") |
|
|
| if resolved_path.exists(): |
| return FileResponse(str(resolved_path), media_type="image/png") |
| raise HTTPException(status_code=404, detail="Image not found") |
|
|
|
|
| @router.post("/{patient_id}/lesions/{lesion_id}/images/{image_id}/analyze") |
| async def analyze_image( |
| patient_id: str, |
| lesion_id: str, |
| image_id: str, |
| question: str = Query(None) |
| ): |
| """Analyze an image with SSE streaming""" |
| store = get_case_store() |
|
|
| |
| img = store.get_image(patient_id, lesion_id, image_id) |
| if not img: |
| raise HTTPException(status_code=404, detail="Image not found") |
| if not img.image_path: |
| raise HTTPException(status_code=400, detail="Image has no file uploaded") |
|
|
| service = get_analysis_service() |
|
|
| async def generate(): |
| try: |
| for chunk in service.analyze(patient_id, lesion_id, image_id, question): |
| yield f"data: {json.dumps(chunk)}\n\n" |
| yield "data: [DONE]\n\n" |
| except Exception as e: |
| yield f"data: {json.dumps(f'[ERROR]{str(e)}[/ERROR]')}\n\n" |
| yield "data: [DONE]\n\n" |
|
|
| return StreamingResponse( |
| generate(), |
| media_type="text/event-stream", |
| headers={ |
| "Cache-Control": "no-cache", |
| "Connection": "keep-alive", |
| } |
| ) |
|
|
|
|
| @router.post("/{patient_id}/lesions/{lesion_id}/images/{image_id}/confirm") |
| async def confirm_diagnosis( |
| patient_id: str, |
| lesion_id: str, |
| image_id: str, |
| confirmed: bool = Query(...), |
| feedback: str = Query(None) |
| ): |
| """Confirm or reject diagnosis and get management guidance""" |
| service = get_analysis_service() |
|
|
| async def generate(): |
| try: |
| for chunk in service.confirm(patient_id, lesion_id, image_id, confirmed, feedback): |
| yield f"data: {json.dumps(chunk)}\n\n" |
| yield "data: [DONE]\n\n" |
| except Exception as e: |
| yield f"data: {json.dumps(f'[ERROR]{str(e)}[/ERROR]')}\n\n" |
| yield "data: [DONE]\n\n" |
|
|
| return StreamingResponse( |
| generate(), |
| media_type="text/event-stream", |
| headers={ |
| "Cache-Control": "no-cache", |
| "Connection": "keep-alive", |
| } |
| ) |
|
|
|
|
| @router.post("/{patient_id}/lesions/{lesion_id}/images/{image_id}/compare") |
| async def compare_to_previous( |
| patient_id: str, |
| lesion_id: str, |
| image_id: str |
| ): |
| """Compare this image to the previous one in the timeline""" |
| store = get_case_store() |
|
|
| |
| current_img = store.get_image(patient_id, lesion_id, image_id) |
| if not current_img: |
| raise HTTPException(status_code=404, detail="Image not found") |
|
|
| previous_img = store.get_previous_image(patient_id, lesion_id, image_id) |
| if not previous_img: |
| raise HTTPException(status_code=400, detail="No previous image to compare") |
|
|
| service = get_analysis_service() |
|
|
| async def generate(): |
| try: |
| for chunk in service.compare_images( |
| patient_id, lesion_id, |
| previous_img.image_path, |
| current_img.image_path, |
| image_id |
| ): |
| yield f"data: {json.dumps(chunk)}\n\n" |
| yield "data: [DONE]\n\n" |
| except Exception as e: |
| yield f"data: {json.dumps(f'[ERROR]{str(e)}[/ERROR]')}\n\n" |
| yield "data: [DONE]\n\n" |
|
|
| return StreamingResponse( |
| generate(), |
| media_type="text/event-stream", |
| headers={ |
| "Cache-Control": "no-cache", |
| "Connection": "keep-alive", |
| } |
| ) |
|
|
|
|
| @router.post("/{patient_id}/lesions/{lesion_id}/chat") |
| async def chat_message( |
| patient_id: str, |
| lesion_id: str, |
| message: dict |
| ): |
| """Send a chat message with SSE streaming response""" |
| store = get_case_store() |
|
|
| lesion = store.get_lesion(patient_id, lesion_id) |
| if not lesion: |
| raise HTTPException(status_code=404, detail="Lesion not found") |
|
|
| service = get_analysis_service() |
| content = message.get("content", "") |
|
|
| async def generate(): |
| try: |
| for chunk in service.chat_followup(patient_id, lesion_id, content): |
| yield f"data: {json.dumps(chunk)}\n\n" |
| yield "data: [DONE]\n\n" |
| except Exception as e: |
| yield f"data: {json.dumps(f'[ERROR]{str(e)}[/ERROR]')}\n\n" |
| yield "data: [DONE]\n\n" |
|
|
| return StreamingResponse( |
| generate(), |
| media_type="text/event-stream", |
| headers={ |
| "Cache-Control": "no-cache", |
| "Connection": "keep-alive", |
| } |
| ) |
|
|