Spaces:
Sleeping
Sleeping
| import os | |
| import io | |
| import json | |
| import base64 | |
| import datetime | |
| import logging | |
| from functools import lru_cache | |
| from fastapi import FastAPI, File, UploadFile, Form, HTTPException | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from PIL import Image, UnidentifiedImageError | |
| from ultralytics import YOLO | |
| from huggingface_hub import HfApi | |
| # ────────────────────────────────────────────── | |
| # Config & logging | |
| # ────────────────────────────────────────────── | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger("afb") | |
| MODEL_REPO = os.environ.get("MODEL_REPO", "") # เช่น "Teeradon/AFB-Detect-YOLOv11" | |
| MODEL_FILE = os.environ.get("MODEL_FILE", "best.pt") | |
| LOCAL_MODEL = os.environ.get("LOCAL_MODEL", "best.pt") | |
| CONF_THRESHOLD = float(os.environ.get("CONF_THRESHOLD", "0.15")) | |
| IMGSZ = int(os.environ.get("IMGSZ", "1536")) # ขนาดภาพตอน inference (ปรับได้ผ่าน env) | |
| MAX_FILE_MB = 10 | |
| ALLOWED_TYPES = {"image/jpeg", "image/png", "image/jpg", "image/webp"} | |
| # ── ที่เก็บภาพ: Hugging Face Dataset ── | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| DATASET_REPO = os.environ.get("DATASET_REPO", "") # เช่น "Teeradon/afb-research-images" | |
| app = FastAPI(title="AFB Detection") | |
| # ────────────────────────────────────────────── | |
| # Startup validation | |
| # ────────────────────────────────────────────── | |
| def validate_config(): | |
| problems = [] | |
| if not HF_TOKEN: | |
| problems.append("HF_TOKEN ไม่ได้ตั้งค่า") | |
| if not DATASET_REPO: | |
| problems.append("DATASET_REPO ไม่ได้ตั้งค่า (ที่เก็บภาพ)") | |
| if problems: | |
| for p in problems: | |
| logger.warning("CONFIG WARNING: %s", p) | |
| else: | |
| logger.info("Config validated OK") | |
| # ────────────────────────────────────────────── | |
| # Lazy-loaded singletons | |
| # ────────────────────────────────────────────── | |
| def get_model(): | |
| """โหลด YOLO model ครั้งเดียว แล้ว cache ไว้""" | |
| if MODEL_REPO: | |
| from huggingface_hub import hf_hub_download | |
| path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE, token=HF_TOKEN) | |
| else: | |
| path = LOCAL_MODEL | |
| logger.info("Loading YOLO model from %s", path) | |
| return YOLO(path) | |
| def get_hf_api(): | |
| """สร้าง HfApi client ครั้งเดียว แล้ว cache ไว้""" | |
| if not HF_TOKEN: | |
| raise RuntimeError("HF_TOKEN ไม่ได้ตั้งค่า") | |
| return HfApi(token=HF_TOKEN) | |
| def upload_to_dataset(img_bytes: bytes, path_in_repo: str): | |
| """อัปโหลดภาพขึ้น HF Dataset repo""" | |
| api = get_hf_api() | |
| api.upload_file( | |
| path_or_fileobj=io.BytesIO(img_bytes), | |
| path_in_repo=path_in_repo, | |
| repo_id=DATASET_REPO, | |
| repo_type="dataset", | |
| ) | |
| # ────────────────────────────────────────────── | |
| # Routes — pages | |
| # ────────────────────────────────────────────── | |
| def read_html(name: str) -> str: | |
| with open(name, "r", encoding="utf-8") as f: | |
| return f.read() | |
| async def index(): | |
| return read_html("index.html") | |
| async def result_page(): | |
| return read_html("result.html") | |
| async def health(): | |
| return {"status": "ok"} | |
| # ────────────────────────────────────────────── | |
| # Route — analyze | |
| # ────────────────────────────────────────────── | |
| async def analyze( | |
| file: UploadFile = File(...), | |
| consent: str = Form("true"), | |
| sample_ref: str = Form(""), | |
| ): | |
| # 1) ตรวจ consent | |
| if consent.lower() != "true": | |
| raise HTTPException(status_code=400, detail="ต้องยินยอมก่อนจึงจะวิเคราะห์ได้") | |
| # 2) ตรวจชนิดไฟล์ | |
| if file.content_type not in ALLOWED_TYPES: | |
| raise HTTPException( | |
| status_code=415, | |
| detail=f"ชนิดไฟล์ไม่รองรับ: {file.content_type} (รองรับ JPG, PNG, WEBP)", | |
| ) | |
| # 3) อ่าน + ตรวจขนาด | |
| img_bytes = await file.read() | |
| if len(img_bytes) > MAX_FILE_MB * 1024 * 1024: | |
| raise HTTPException(status_code=413, detail=f"ไฟล์ใหญ่เกิน {MAX_FILE_MB}MB") | |
| # 4) เปิดภาพ | |
| try: | |
| img = Image.open(io.BytesIO(img_bytes)).convert("RGB") | |
| except (UnidentifiedImageError, OSError): | |
| raise HTTPException(status_code=422, detail="ไม่สามารถเปิดภาพได้ (ไฟล์อาจเสียหาย)") | |
| # 5) รัน inference | |
| try: | |
| model = get_model() | |
| results = model(img, conf=CONF_THRESHOLD, imgsz=IMGSZ, verbose=False) | |
| result = results[0] | |
| except Exception: | |
| logger.exception("Inference failed") | |
| raise HTTPException(status_code=500, detail="การวิเคราะห์ล้มเหลว กรุณาลองใหม่") | |
| # 6) สรุปผล + เก็บ raw boxes ทุกอันไว้ให้ frontend กรองเอง | |
| img_w, img_h = img.size | |
| all_boxes = [] | |
| if len(result.boxes) > 0: | |
| xyxy = result.boxes.xyxy.tolist() | |
| confs_raw = result.boxes.conf.tolist() | |
| for (x1, y1, x2, y2), c in zip(xyxy, confs_raw): | |
| all_boxes.append({ | |
| "x1": round(x1 / img_w, 4), | |
| "y1": round(y1 / img_h, 4), | |
| "x2": round(x2 / img_w, 4), | |
| "y2": round(y2 / img_h, 4), | |
| "conf": round(c, 4), | |
| }) | |
| all_boxes.sort(key=lambda b: b["conf"], reverse=True) | |
| boxes_at_default = [b for b in all_boxes if b["conf"] >= CONF_THRESHOLD] | |
| afb_count = len(boxes_at_default) | |
| avg_conf = round(sum(b["conf"] for b in boxes_at_default) / afb_count * 100, 1) if afb_count > 0 else 0.0 | |
| # 7) ส่งภาพต้นฉบับ (ไม่มี box) — frontend วาด box เองจาก all_boxes | |
| buf = io.BytesIO() | |
| img.save(buf, format="JPEG", quality=90) | |
| orig_jpeg = buf.getvalue() | |
| orig_b64 = base64.b64encode(orig_jpeg).decode() | |
| # 8) บันทึกขึ้น HF Dataset — error ไม่ทำให้ทั้ง request ล้ม | |
| saved = False | |
| save_error = None | |
| try: | |
| now = datetime.datetime.now() | |
| ts = now.strftime("%Y%m%d_%H%M%S") | |
| date_folder = now.strftime("%Y-%m-%d") | |
| ref = (sample_ref or "noref").replace("/", "-").replace(" ", "_")[:40] | |
| base_name = f"images/{date_folder}/afb_{ts}_{ref}_n{afb_count}_c{avg_conf}" | |
| upload_to_dataset(orig_jpeg, f"{base_name}_original.jpg") | |
| try: | |
| det_img = Image.fromarray(result.plot()[..., ::-1]) | |
| det_buf = io.BytesIO() | |
| det_img.save(det_buf, format="JPEG", quality=90) | |
| upload_to_dataset(det_buf.getvalue(), f"{base_name}_detection.jpg") | |
| except Exception: | |
| pass | |
| saved = True | |
| except Exception: | |
| logger.exception("Dataset upload failed") | |
| save_error = "บันทึกภาพไม่สำเร็จ (ผลการวิเคราะห์ยังแสดงได้ปกติ)" | |
| return JSONResponse({ | |
| "afb_count": afb_count, | |
| "avg_conf": avg_conf, | |
| "orig_b64": orig_b64, | |
| "all_boxes": all_boxes, | |
| "conf_threshold": CONF_THRESHOLD, | |
| "saved": saved, | |
| "save_error": save_error, | |
| "sample_ref": sample_ref or "", | |
| }) |