Spaces:
Sleeping
Sleeping
Download api.py from aryarasyad/GLPFish_old: direct link, hf CLI and curl.
- Browser
- Download file 9.04 kB
-
https://huggingface.co/spaces/aryarasyad/GLPFish_old/resolve/main/api.py
- Command line
-
hf download hf://spaces/aryarasyad/GLPFish_old/api.py
-
curl -L -o api.py https://huggingface.co/spaces/aryarasyad/GLPFish_old/resolve/main/api.py
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 βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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(), | |
| ) | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def not_found(request: Request, exc): | |
| return JSONResponse( | |
| status_code=404, | |
| content={"detail": f"Endpoint '{request.url.path}' tidak ditemukan."} | |
| ) | |
| async def server_error(request: Request, exc): | |
| return JSONResponse( | |
| status_code=500, | |
| content={"detail": "Internal server error. Cek log server."} | |
| ) |