Spaces:
Running
Running
| from pathlib import Path | |
| from fastapi import APIRouter, File, Form, HTTPException, UploadFile | |
| from fastapi.responses import JSONResponse | |
| from config import config | |
| from logging_config import get_logger | |
| from models.schemas import ServiceStatusResponse | |
| from services.vision_service import vision_service | |
| logger = get_logger("routers.vision_router") | |
| router = APIRouter(prefix="/v1/vision", tags=["Vision AI"]) | |
| async def analyze_image( | |
| image: UploadFile = File(..., description="Image file to analyze"), | |
| prompt: str = Form(..., description="Question or instruction about the image"), | |
| temperature: float = Form(0.6, ge=0.0, le=2.0, description="Sampling temperature"), | |
| max_tokens: int = Form(512, ge=1, le=4096, description="Maximum tokens to generate"), | |
| return_json: bool = Form(False, description="Return structured JSON output"), | |
| ) -> JSONResponse: | |
| logger.info( | |
| "Received vision analysis request: filename='%s', prompt_preview='%s...', " | |
| "return_json=%s", | |
| image.filename, | |
| prompt[:60], | |
| return_json, | |
| ) | |
| if not vision_service.is_ready(): | |
| logger.warning("Vision analysis request rejected: vision model not ready") | |
| raise HTTPException(status_code=503, detail="Vision model is not ready") | |
| _validate_image_extension(image.filename) | |
| image_data = await image.read() | |
| logger.info("Image data read: %d bytes", len(image_data)) | |
| _validate_file_size(len(image_data)) | |
| try: | |
| result = await vision_service.analyze_image( | |
| image_data=image_data, | |
| prompt=prompt, | |
| temperature=temperature, | |
| max_tokens=max_tokens, | |
| return_json=return_json, | |
| ) | |
| logger.info("Vision analysis request fulfilled successfully") | |
| return JSONResponse(content=result) | |
| except HTTPException: | |
| raise | |
| except RuntimeError as exc: | |
| logger.error("Runtime error during image analysis: %s", exc) | |
| raise HTTPException(status_code=503, detail=str(exc)) from exc | |
| except Exception as exc: | |
| logger.exception("Unexpected error during image analysis") | |
| raise HTTPException(status_code=500, detail="Internal server error") from exc | |
| def _validate_image_extension(filename: str) -> None: | |
| extension = Path(filename).suffix.lower() | |
| if extension not in config.ALLOWED_IMAGE_EXTENSIONS: | |
| allowed = ", ".join(sorted(config.ALLOWED_IMAGE_EXTENSIONS)) | |
| logger.warning( | |
| "Rejected file upload with unsupported extension '%s'. Allowed: %s", | |
| extension, | |
| allowed, | |
| ) | |
| raise HTTPException( | |
| status_code=400, | |
| detail=f"Unsupported file type '{extension}'. Allowed extensions: {allowed}", | |
| ) | |
| def _validate_file_size(size_bytes: int) -> None: | |
| if size_bytes > config.MAX_FILE_SIZE_BYTES: | |
| max_mb = config.MAX_FILE_SIZE_BYTES / (1024 * 1024) | |
| logger.warning( | |
| "Rejected file upload: size %d bytes exceeds limit of %.0f MB", | |
| size_bytes, | |
| max_mb, | |
| ) | |
| raise HTTPException( | |
| status_code=400, | |
| detail=f"File size exceeds the maximum allowed limit of {max_mb:.0f} MB", | |
| ) | |
| async def vision_health() -> ServiceStatusResponse: | |
| ready = vision_service.is_ready() | |
| logger.debug("Vision health check: ready=%s", ready) | |
| return ServiceStatusResponse( | |
| status="healthy" if ready else "initializing", | |
| model_ready=ready, | |
| ) | |