From 2e30e0039ee40acf324580a16fe41b112aebf3a7 Mon Sep 17 00:00:00 2001 From: Mohammad Azri Date: Sat, 27 Sep 2025 14:19:37 +0800 Subject: [PATCH] Updated backend for 2 seperate api link --- .gitignore | 3 + backend/app/routes/report.py | 188 ++++++++++-------- .../e604f450-53b1-4400-8a3c-a22893208a7b.jpg | Bin 0 -> 16968 bytes 3 files changed, 111 insertions(+), 80 deletions(-) create mode 100644 backend/static/uploads/e604f450-53b1-4400-8a3c-a22893208a7b.jpg diff --git a/.gitignore b/.gitignore index d1ffd1e..c7ad317 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ migrate_working_dir/ # AI Memory Bank - keep private .kilocode/ +#ignore all venv +venv/ + # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id diff --git a/backend/app/routes/report.py b/backend/app/routes/report.py index ccf94ff..2ad2900 100644 --- a/backend/app/routes/report.py +++ b/backend/app/routes/report.py @@ -2,13 +2,14 @@ from typing import Optional from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException, Request from fastapi.responses import JSONResponse from sqlalchemy.orm import Session +from pathlib import Path +import logging, uuid + from app.database import get_db from app.services.ticket_service import TicketService, SeverityLevel from app.models.ticket_model import User from app.services.global_ai import get_ai_service from app.utils import make_image_url, normalize_image_path_for_url -from pathlib import Path -import logging, uuid router = APIRouter() logger = logging.getLogger(__name__) @@ -17,6 +18,80 @@ logger.setLevel(logging.DEBUG) UPLOAD_DIR = Path("static") / "uploads" UPLOAD_DIR.mkdir(parents=True, exist_ok=True) +# ---------------------- +# API 1: Analyze image (no DB write) +# ---------------------- +@router.post("/analyze") +async def analyze_image( + image: UploadFile = File(...), + request: Request = None +): + logger.debug("Received analyze request") + + # Validate file extension and type + allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'} + allowed_content_types = { + 'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', + 'application/octet-stream' + } + + file_ext = Path(image.filename).suffix.lower() + if file_ext not in allowed_extensions: + raise HTTPException(status_code=400, detail="Only image files are allowed") + if image.content_type not in allowed_content_types: + raise HTTPException(status_code=400, detail="Invalid file type") + + # Save file + filename = f"{uuid.uuid4()}{file_ext}" + file_path_obj = UPLOAD_DIR / filename + try: + content = await image.read() + file_path_obj.write_bytes(content) + logger.debug(f"Saved image for analysis: {file_path_obj}") + except Exception: + logger.exception("Failed to save image for analysis") + raise HTTPException(status_code=500, detail="Failed to save uploaded image") + + # Run AI + ai_service = get_ai_service() + try: + category = ai_service.classify_category(str(file_path_obj)) + logger.debug(f"Classification result: {category}") + + severity = SeverityLevel.NA + annotated_path = None + if category.lower() == "pothole": + severity_str, annotated_path = ai_service.detect_pothole_severity(str(file_path_obj)) + severity = { + "High": SeverityLevel.HIGH, + "Medium": SeverityLevel.MEDIUM, + "Low": SeverityLevel.LOW, + "Unknown": SeverityLevel.NA + }.get(severity_str, SeverityLevel.NA) + logger.debug(f"Severity detection: {severity_str}") + except Exception: + logger.exception("AI analysis failed") + category = "Unknown" + severity = SeverityLevel.NA + + rel_path = normalize_image_path_for_url(file_path_obj.as_posix()) + image_url = make_image_url(rel_path, request) + + response = { + "temp_id": str(uuid.uuid4()), + "filename": filename, + "image_path": rel_path, + "image_url": image_url, + "category": category, + "severity": severity.value + } + logger.debug(f"Analyze response: {response}") + return JSONResponse(status_code=200, content=response) + + +# ---------------------- +# API 2: Submit report (with analyzed file + DB write) +# ---------------------- @router.post("/report") async def report_issue( user_id: Optional[str] = Form(None), @@ -25,104 +100,58 @@ async def report_issue( longitude: float = Form(...), address: Optional[str] = Form(None), description: str = Form(""), - image: UploadFile = File(...), - request: Request = None, - db: Session = Depends(get_db) + analyzed_file: str = Form(...), # filename returned from /analyze + category: str = Form(...), + severity: str = Form(...), + db: Session = Depends(get_db), + request: Request = None ): - logger.debug("Received report request") + logger.debug("Received report submission request") ticket_service = TicketService(db) - # Validate or create user + # Ensure user user = None if user_id: user = ticket_service.get_user(user_id) if not user: - # Create a guest user automatically guest_email = f"guest-{uuid.uuid4()}@example.local" guest_name = user_name or f"Guest-{str(uuid.uuid4())[:8]}" try: user = ticket_service.create_user(name=guest_name, email=guest_email) logger.info(f"Created guest user: {user}") - except Exception as e: + except Exception: logger.exception("Failed to create guest user") raise HTTPException(status_code=500, detail="Failed to ensure user") - logger.debug(f"Using user: {user.name} ({user.email})") + # Verify analyzed file exists + file_path_obj = UPLOAD_DIR / analyzed_file + if not file_path_obj.exists(): + logger.error(f"Analyzed file not found: {analyzed_file}") + raise HTTPException(status_code=400, detail="Analyzed file not found") - # Validate file type - allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'} - allowed_content_types = { - 'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', - 'application/octet-stream' # Some cameras/mobile devices use this - } - - file_ext = Path(image.filename).suffix.lower() - if file_ext not in allowed_extensions: - logger.error(f"Invalid file extension: {file_ext}") - raise HTTPException(status_code=400, detail="Only image files are allowed") - - if image.content_type not in allowed_content_types: - logger.error(f"Invalid content type: {image.content_type}") - raise HTTPException(status_code=400, detail="Invalid file type") - - # Save uploaded image - filename = f"{uuid.uuid4()}{file_ext}" - file_path_obj = UPLOAD_DIR / filename + # Save ticket + severity_enum = SeverityLevel.__members__.get(severity.upper(), SeverityLevel.NA) try: - content = await image.read() - file_path_obj.write_bytes(content) - logger.debug(f"Saved image to {file_path_obj} ({len(content)} bytes)") - except Exception as e: - logger.exception("Failed to save uploaded image") - raise HTTPException(status_code=500, detail="Failed to save uploaded image") + ticket = ticket_service.create_ticket( + user_id=user.id, + image_path=file_path_obj.as_posix(), + category=category, + severity=severity_enum, + latitude=latitude, + longitude=longitude, + description=description, + address=address + ) + logger.info(f"Ticket created: {ticket.id} for user {user.id}") + except Exception: + logger.exception("Failed to create ticket") + raise HTTPException(status_code=500, detail="Failed to create ticket") - # Get initialized AI service - ai_service = get_ai_service() - logger.debug("AI service ready") - - # Run AI predictions - try: - category = ai_service.classify_category(str(file_path_obj)) - logger.debug(f"Classification: {category}") - - if category.lower() == "pothole": - severity_str, annotated_path = ai_service.detect_pothole_severity(str(file_path_obj)) - logger.debug(f"Detection: severity={severity_str}, path={annotated_path}") - severity = { - "High": SeverityLevel.HIGH, - "Medium": SeverityLevel.MEDIUM, - "Low": SeverityLevel.LOW, - "Unknown": SeverityLevel.NA - }.get(severity_str, SeverityLevel.NA) - else: - severity = SeverityLevel.NA - logger.debug("No detection needed") - except Exception as e: - logger.exception("AI prediction failed") - category = "Unknown" - severity = SeverityLevel.NA - - # Create ticket (store relative posix path) - image_path_db = file_path_obj.as_posix() - ticket = ticket_service.create_ticket( - user_id=user.id, - image_path=image_path_db, - category=category, - severity=severity, - latitude=latitude, - longitude=longitude, - description=description, - address=address - ) - logger.info(f"Ticket created: {ticket.id} for user {user.id}") - - # Normalize stored path and build absolute URL rel_path = normalize_image_path_for_url(ticket.image_path) image_url = make_image_url(rel_path, request) - + response = { "ticket_id": ticket.id, - "id": ticket.id, "user_id": user.id, "user_name": user.name, "user_email": user.email, @@ -134,6 +163,5 @@ async def report_issue( "image_url": image_url, "address": ticket.address } - - logger.debug(f"Response: {response}") + logger.debug(f"Report response: {response}") return JSONResponse(status_code=201, content=response) diff --git a/backend/static/uploads/e604f450-53b1-4400-8a3c-a22893208a7b.jpg b/backend/static/uploads/e604f450-53b1-4400-8a3c-a22893208a7b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7bfe8d856003d063a5006e2d7e9c51a27697fe41 GIT binary patch literal 16968 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