Spaces:
Sleeping
Sleeping
File size: 9,043 Bytes
ad61cf0 98af918 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 529a7e4 ad61cf0 98af918 ad61cf0 98af918 529a7e4 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 98af918 ad61cf0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | """
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."}
) |