GLPFish_old / api.py
Kemas M. Aryadary Rasyad
fix again
98af918
Raw History Blame Contribute Delete
9.04 kB
"""
Fish Quality Check β€” FastAPI Backend
=====================================
Endpoint:
POST /predict β†’ kirim foto mata + insang, terima grade A/B/C
GET /health β†’ cek status server & model
GET /docs β†’ dokumentasi API otomatis (Swagger UI)
Jalankan:
uvicorn api:app --host 0.0.0.0 --port 8000 --reload
"""
import io
import os
import sys
import time
import logging
from datetime import datetime
from typing import Optional
from fastapi import FastAPI, File, UploadFile, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from PIL import Image
# Pastikan utils bisa diimport dari mana pun uvicorn dijalankan
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE_DIR)
from utils.inference import classify_fish, is_model_ready, load_model
# ── Logging ──────────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
logger = logging.getLogger(__name__)
# ── App ───────────────────────────────────────────────────────────────────────
app = FastAPI(
title="Fish Quality Check API",
description="""
## API untuk grading kualitas ikan ekspor
Upload foto **mata** dan **insang** ikan, dapatkan **Grade A / B / C** secara otomatis.
### Logika Grading
| Mata | Insang | Grade | Keterangan |
|----------|-----------|-------|-----------------|
| Segar | Segar | **A** | Sangat Segar |
| Segar | Tdk Segar | **B** | Cukup Segar |
| Tdk Segar| Segar | **B** | Cukup Segar |
| Tdk Segar| Tdk Segar | **C** | Tidak Segar |
""",
version="1.0.0",
)
# ── CORS ─────────────────────────────────────────────────────────────────────
# Tambahkan URL frontend kamu di sini
ALLOWED_ORIGINS = [
"http://localhost:3000", # React dev server
"http://localhost:5173", # Vite dev server
"http://localhost:8080", # Vue dev server
"https://your-frontend.com", # Ganti dengan domain produksi
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["*"],
)
# ── Response schemas (Pydantic) ───────────────────────────────────────────────
class PartResult(BaseModel):
kelas: str
status: str
confidence: float
probabilitas: dict
class PredictResponse(BaseModel):
grade: str
label: str
warna: str
deskripsi: str
layak_ekspor: bool
mata: PartResult
insang: PartResult
waktu_proses_ms: float
timestamp: str
class HealthResponse(BaseModel):
status: str
model_ready: bool
device: str
timestamp: str
# ── Startup: pre-load model ───────────────────────────────────────────────────
@app.on_event("startup")
async def startup_event():
logger.info("Server starting up...")
if is_model_ready():
try:
load_model()
logger.info("Model loaded successfully on startup.")
except Exception as e:
logger.warning(f"Model load failed on startup: {e}")
else:
logger.warning("Model file not found. Run train.py first.")
# ── Middleware: log setiap request ────────────────────────────────────────────
@app.middleware("http")
async def log_requests(request: Request, call_next):
start = time.time()
response = await call_next(request)
duration = (time.time() - start) * 1000
logger.info(f"{request.method} {request.url.path} β†’ {response.status_code} ({duration:.1f}ms)")
return response
# ── Helper: validasi file upload ──────────────────────────────────────────────
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/jpg", "image/webp"}
MAX_SIZE_MB = 10
def validate_image(file: UploadFile, field_name: str) -> None:
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=400,
detail=f"Format file '{field_name}' tidak didukung. Gunakan JPG atau PNG."
)
async def read_image(file: UploadFile, field_name: str) -> Image.Image:
validate_image(file, field_name)
raw = await file.read()
if len(raw) > MAX_SIZE_MB * 1024 * 1024:
raise HTTPException(
status_code=400,
detail=f"Ukuran file '{field_name}' terlalu besar. Maksimal {MAX_SIZE_MB}MB."
)
try:
img = Image.open(io.BytesIO(raw)).convert("RGB")
except Exception:
raise HTTPException(
status_code=400,
detail=f"File '{field_name}' tidak dapat dibaca sebagai gambar."
)
return img
# ── Endpoints ─────────────────────────────────────────────────────────────────
@app.get("/health", response_model=HealthResponse, tags=["System"])
def health():
"""Cek status server dan apakah model sudah siap."""
import torch
device = "cuda" if torch.cuda.is_available() else "cpu"
return HealthResponse(
status="ok",
model_ready=is_model_ready(),
device=device,
timestamp=datetime.now().isoformat(),
)
@app.post(
"/predict",
response_model=PredictResponse,
tags=["Prediction"],
summary="Prediksi grade kualitas ikan",
responses={
200: {"description": "Prediksi berhasil"},
400: {"description": "File tidak valid"},
503: {"description": "Model belum siap"},
},
)
async def predict(
eye: UploadFile = File(..., description="Foto mata ikan (JPG/PNG)"),
gill: UploadFile = File(..., description="Foto insang ikan (JPG/PNG)"),
):
"""
**Upload dua gambar:**
- `eye` β†’ foto close-up mata ikan
- `gill` β†’ foto close-up insang ikan
**Response:** grade A / B / C beserta detail confidence per bagian.
"""
# Cek model
if not is_model_ready():
raise HTTPException(
status_code=503,
detail="Model belum tersedia. Jalankan train.py terlebih dahulu."
)
# Baca & validasi gambar
eye_img = await read_image(eye, "eye")
gill_img = await read_image(gill, "gill")
# Jalankan inferensi
try:
start_t = time.time()
result = classify_fish(eye_img, gill_img)
elapsed_ms = (time.time() - start_t) * 1000
except Exception as e:
logger.error(f"Inference error: {e}")
raise HTTPException(
status_code=500,
detail=f"Terjadi error saat analisis: {str(e)}"
)
# Susun response
return PredictResponse(
grade = result["grade"],
label = result["label"],
warna = result["color"],
deskripsi = result["description"],
layak_ekspor = result["grade"] in ("A", "B"),
mata=PartResult(
kelas = result["eye"]["class"],
status = result["eye"]["status"],
confidence = round(result["eye"]["confidence"], 4),
probabilitas = {k: round(v, 4) for k, v in result["eye"]["probs"].items()},
),
insang=PartResult(
kelas = result["gill"]["class"],
status = result["gill"]["status"],
confidence = round(result["gill"]["confidence"], 4),
probabilitas = {k: round(v, 4) for k, v in result["gill"]["probs"].items()},
),
waktu_proses_ms = round(elapsed_ms, 2),
timestamp = datetime.now().isoformat(),
)
# ── Error handlers ────────────────────────────────────────────────────────────
@app.exception_handler(404)
async def not_found(request: Request, exc):
return JSONResponse(
status_code=404,
content={"detail": f"Endpoint '{request.url.path}' tidak ditemukan."}
)
@app.exception_handler(500)
async def server_error(request: Request, exc):
return JSONResponse(
status_code=500,
content={"detail": "Internal server error. Cek log server."}
)