Compare commits

3 Commits

Author SHA1 Message Date
dbf7be38f7 Merge branch 'main' into chat_bot 2025-09-27 15:04:14 +08:00
f177f075bc ww 2025-09-27 15:02:39 +08:00
Mohammad Azri
2e30e0039e Updated backend for 2 seperate api link 2025-09-27 14:19:37 +08:00
4 changed files with 112 additions and 81 deletions

3
.gitignore vendored
View File

@@ -26,6 +26,9 @@ migrate_working_dir/
# AI Memory Bank - keep private # AI Memory Bank - keep private
.kilocode/ .kilocode/
#ignore all venv
venv/
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
**/doc/api/ **/doc/api/
**/ios/Flutter/.last_build_id **/ios/Flutter/.last_build_id

View File

@@ -4,7 +4,7 @@ CityPulse is a comprehensive citizen reporting application that combines **Flutt
## 🎯 System Architecture ## 🎯 System Architecture
### Frontend (Flutter) ### Frontend (Flutterrr)
- **Location**: Root directory - **Location**: Root directory
- **Technology**: Flutter (Dart) with Material Design - **Technology**: Flutter (Dart) with Material Design
- **Purpose**: Cross-platform mobile and web interface for citizens - **Purpose**: Cross-platform mobile and web interface for citizens

View File

@@ -2,13 +2,14 @@ from typing import Optional
from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException, Request from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from pathlib import Path
import logging, uuid
from app.database import get_db from app.database import get_db
from app.services.ticket_service import TicketService, SeverityLevel from app.services.ticket_service import TicketService, SeverityLevel
from app.models.ticket_model import User from app.models.ticket_model import User
from app.services.global_ai import get_ai_service from app.services.global_ai import get_ai_service
from app.utils import make_image_url, normalize_image_path_for_url from app.utils import make_image_url, normalize_image_path_for_url
from pathlib import Path
import logging, uuid
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -17,6 +18,80 @@ logger.setLevel(logging.DEBUG)
UPLOAD_DIR = Path("static") / "uploads" UPLOAD_DIR = Path("static") / "uploads"
UPLOAD_DIR.mkdir(parents=True, exist_ok=True) 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") @router.post("/report")
async def report_issue( async def report_issue(
user_id: Optional[str] = Form(None), user_id: Optional[str] = Form(None),
@@ -25,104 +100,58 @@ async def report_issue(
longitude: float = Form(...), longitude: float = Form(...),
address: Optional[str] = Form(None), address: Optional[str] = Form(None),
description: str = Form(""), description: str = Form(""),
image: UploadFile = File(...), analyzed_file: str = Form(...), # filename returned from /analyze
request: Request = None, category: str = Form(...),
db: Session = Depends(get_db) 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) ticket_service = TicketService(db)
# Validate or create user # Ensure user
user = None user = None
if user_id: if user_id:
user = ticket_service.get_user(user_id) user = ticket_service.get_user(user_id)
if not user: if not user:
# Create a guest user automatically
guest_email = f"guest-{uuid.uuid4()}@example.local" guest_email = f"guest-{uuid.uuid4()}@example.local"
guest_name = user_name or f"Guest-{str(uuid.uuid4())[:8]}" guest_name = user_name or f"Guest-{str(uuid.uuid4())[:8]}"
try: try:
user = ticket_service.create_user(name=guest_name, email=guest_email) user = ticket_service.create_user(name=guest_name, email=guest_email)
logger.info(f"Created guest user: {user}") logger.info(f"Created guest user: {user}")
except Exception as e: except Exception:
logger.exception("Failed to create guest user") logger.exception("Failed to create guest user")
raise HTTPException(status_code=500, detail="Failed to ensure 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 # Save ticket
allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'} severity_enum = SeverityLevel.__members__.get(severity.upper(), SeverityLevel.NA)
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
try: try:
content = await image.read() ticket = ticket_service.create_ticket(
file_path_obj.write_bytes(content) user_id=user.id,
logger.debug(f"Saved image to {file_path_obj} ({len(content)} bytes)") image_path=file_path_obj.as_posix(),
except Exception as e: category=category,
logger.exception("Failed to save uploaded image") severity=severity_enum,
raise HTTPException(status_code=500, detail="Failed to save uploaded image") 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) rel_path = normalize_image_path_for_url(ticket.image_path)
image_url = make_image_url(rel_path, request) image_url = make_image_url(rel_path, request)
response = { response = {
"ticket_id": ticket.id, "ticket_id": ticket.id,
"id": ticket.id,
"user_id": user.id, "user_id": user.id,
"user_name": user.name, "user_name": user.name,
"user_email": user.email, "user_email": user.email,
@@ -134,6 +163,5 @@ async def report_issue(
"image_url": image_url, "image_url": image_url,
"address": ticket.address "address": ticket.address
} }
logger.debug(f"Report response: {response}")
logger.debug(f"Response: {response}")
return JSONResponse(status_code=201, content=response) return JSONResponse(status_code=201, content=response)

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB