Spaces:
Sleeping
Sleeping
File size: 7,000 Bytes
8857eb3 d077885 8857eb3 |
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 |
import torch
import cv2
import numpy as np
import joblib
import os
import uvicorn
from fastapi import FastAPI, UploadFile, File
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from io import BytesIO
from PIL import Image
from transformers import AutoImageProcessor, AutoModel
from ultralytics import YOLO
from sklearn.preprocessing import MultiLabelBinarizer
import asyncio
# ======================================================
# 0. KONFIGURASI DAN MUAT MODEL (PATH HARUS RELATIF)
# ======================================================
# Catatan: Hugging Face Spaces akan mencari file di direktori ini (BASE_PATH = .)
BASE_PATH = "."
YOLO_MODEL_PATH = os.path.join(BASE_PATH, "best.pt")
# Hapus penamaan tanggal yang panjang di Colab dan gunakan nama folder sebenarnya:
MODEL_DIR = os.path.join(BASE_PATH, "OVR_Checkpoints-20251018T053026Z-1-001", "OVR_Checkpoints")
ENCODER_PATH = os.path.join(BASE_PATH, "dinov3_multilabel_encoder.pkl")
MAPPING_SAVE_PATH = os.path.join(BASE_PATH, "label_mapping_dict.joblib")
label_columns = ["product", "grade", "cap", "label", "brand", "type", "subtype", "volume"]
device = torch.device("cpu") # Gunakan CPU untuk stabilitas di HF Spaces (kecuali Anda memilih GPU di settings)
# --- SETUP DINOv3 Auth Token ---
# Membaca token dari Environment Variable yang Anda set di Secrets HF Space
HF_AUTH_TOKEN = os.environ.get("HUGGINGFACE_TOKEN")
# --- FUNGSI EKSTRAKSI DINOv3 (Harus didefinisikan sebelum loading) ---
def extract_dinov3_features(image_crop):
inputs = dinov3_processor(images=image_crop, return_tensors="pt").to(device)
with torch.no_grad():
outputs = dinov3_model(**inputs)
cls_token_features = outputs.last_hidden_state[:, 0, :]
return cls_token_features.cpu().numpy().flatten()
# === LOAD SEMUA MODEL KE MEMORI ===
try:
# 1. Load YOLO
yolo_model = YOLO(YOLO_MODEL_PATH)
# 2. Load DINOv3 Processor dan Model (Wajib menggunakan token)
dinov3_model_name = "facebook/dinov3-convnext-small-pretrain-lvd1689m"
dinov3_processor = AutoImageProcessor.from_pretrained(dinov3_model_name, token=HF_AUTH_TOKEN)
dinov3_model = AutoModel.from_pretrained(dinov3_model_name, token=HF_AUTH_TOKEN).to(device).eval()
# 3. Load Scikit-learn Assets
mlb = joblib.load(ENCODER_PATH)
mapping_dict = joblib.load(MAPPING_SAVE_PATH)
num_classes = len(mlb.classes_)
all_classifiers = []
for i in range(num_classes):
class_name = mlb.classes_[i]
safe_class_name = str(class_name).replace(' ', '_').replace('/', '_').replace(':', '_').replace('.', '_')
classifier_path = os.path.join(MODEL_DIR, f"clf_{i}_{safe_class_name}.pkl")
if not os.path.exists(classifier_path):
classifier_path = os.path.join(MODEL_DIR, f"clf_{i}_{class_name}.pkl")
all_classifiers.append(joblib.load(classifier_path))
print("β
Semua model dan aset dimuat ke memori.")
except Exception as e:
# Jika gagal memuat, cetak error dan biarkan API crash (sesuai standar deployment)
print(f"β GAGAL MEMUAT MODEL SECARA LOKAL: {e}")
# Anda mungkin perlu menyesuaikan error handling untuk deployment
exit(1)
# ======================================================
# 1. INISIASI API & MIDDLEWARE
# ======================================================
app = FastAPI(title="HF Product Classifier API")
# Middleware CORS (Wajib untuk Lovable.dev)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ======================================================
# 2. ENDPOINT PREDIKSI UTAMA (LOGIKA PENUH)
# ======================================================
@app.post("/predict")
async def predict_product(file: UploadFile = File(...)):
"""Menerima gambar, menjalankan pipeline, dan mengembalikan JSON."""
try:
# 1. Persiapan Gambar
contents = await file.read()
img_pil = Image.open(BytesIO(contents)).convert("RGB")
# 2. Deteksi YOLOv10m
results = yolo_model(img_pil, verbose=False)
if not results or not results[0].boxes:
return JSONResponse(status_code=200, content={"status": "failed", "message": "No object detected."})
# Ambil BBOX terbaik, Crop, Ekstraksi DINOv3
best_box = results[0].boxes.cpu().numpy()[np.argmax(results[0].boxes.cpu().numpy().conf)]
x_min, y_min, x_max, y_max = map(int, best_box.xyxy[0])
confidence_score = float(best_box.conf[0]) # Confidence YOLO
image_crop = img_pil.crop((x_min, y_min, x_max, y_max))
features = extract_dinov3_features(image_crop)
X_pred = features.reshape(1, -1)
# 3. Klasifikasi (Probabilitas)
Y_proba_list = [clf.predict_proba(X_pred)[0, 1] for clf in all_classifiers]
Y_proba = np.array(Y_proba_list)
Y_pred_biner = (Y_proba > 0.5).astype(int).reshape(1, -1)
class_proba_map = dict(zip(mlb.classes_, Y_proba))
predicted_labels_set = set(mlb.inverse_transform(Y_pred_biner)[0])
final_output = {}
# 4. Logika Pemetaan & Fallback
for col in label_columns:
intersection = predicted_labels_set.intersection(mapping_dict[col])
best_proba = -1
best_label = "UNKNOWN"
for label in mapping_dict[col]:
proba = class_proba_map.get(label, 0.0)
if proba > best_proba:
best_proba = proba
best_label = label
if len(intersection) == 1:
final_output[col] = intersection.pop()
elif len(intersection) == 0:
# UNKNOWN: Fallback ke proba tertinggi jika > 20%
if best_proba > 0.20:
final_output[col] = f"{best_label} ({best_proba*100:.1f}%)"
else:
final_output[col] = "UNKNOWN"
else:
# KONFLIK: Pilih label proba tertinggi
final_output[col] = f"CONFLICT -> {best_label} ({best_proba*100:.1f}%)"
# 5. Kembalikan Respons JSON Penuh
return JSONResponse(status_code=200, content={
"status": "success",
"confidence_score": f"{confidence_score*100:.2f}%",
"prediction": final_output # Semua 8 atribut terstruktur
})
except Exception as e:
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
# ======================================================
# 3. RUN SERVER (ENTRY POINT UNTUK HUGGING FACE)
# ======================================================
# Hugging Face Spaces secara otomatis menjalankan Gunicorn/Uvicorn yang menunjuk ke 'app'.
# Di lokal, Anda bisa mengujinya dengan baris ini:
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000) |