Spaces:
Runtime error
Runtime error
| import os | |
| import io | |
| import json | |
| import base64 | |
| import asyncio | |
| import numpy as np | |
| from PIL import Image | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, HTTPException, status, Header, Depends | |
| from fastapi.responses import JSONResponse | |
| from pydantic import BaseModel | |
| import onnxruntime as ort | |
| # Add parent dir to path so we can import registry module | |
| import sys | |
| from pathlib import Path | |
| sys.path.append(str(Path(__file__).parent.parent)) | |
| from registry.download_model import download_artifacts, TARGET_DIR, VERSION | |
| # Global state | |
| is_model_ready = False | |
| ort_session = None | |
| class_labels = [] | |
| temperature = 1.0 | |
| INTERNAL_SECRET = os.environ.get("INTERNAL_SECRET") | |
| if not INTERNAL_SECRET: | |
| raise ValueError("CRITICAL: INTERNAL_SECRET environment variable is missing.") | |
| async def lifespan(app: FastAPI): | |
| global is_model_ready, ort_session, class_labels, temperature | |
| # 1. Download artifacts | |
| # NOTE: In production on HF Spaces, HF_TOKEN must be set if repo is private | |
| # We run this in a thread to not block the event loop | |
| print("Initializing Hugging Face Space Lifespan...") | |
| try: | |
| await asyncio.to_thread(download_artifacts) | |
| except Exception as e: | |
| print(f"Failed to download artifacts: {e}") | |
| # 2. Load the ONNX model | |
| onnx_path = TARGET_DIR / f"cropguard_{VERSION}.onnx" | |
| if not onnx_path.exists(): | |
| print(f"Warning: Model not found at {onnx_path}. Space will fail health checks.") | |
| else: | |
| # Load ONNX model | |
| providers = ['CPUExecutionProvider'] | |
| ort_session = ort.InferenceSession(str(onnx_path), providers=providers) | |
| # Load labels | |
| json_path = TARGET_DIR / f"cropguard_{VERSION}.json" | |
| if json_path.exists(): | |
| with open(json_path, 'r') as f: | |
| metadata = json.load(f) | |
| class_labels = metadata.get("classes", []) | |
| # Load temperature (optional) | |
| temp_path = TARGET_DIR / "temperature.json" | |
| if temp_path.exists(): | |
| with open(temp_path, 'r') as f: | |
| temperature = json.load(f).get("temperature", 1.0) | |
| is_model_ready = True | |
| print("Model loaded successfully. Engine ready.") | |
| yield | |
| app = FastAPI(lifespan=lifespan) | |
| class InferenceRequest(BaseModel): | |
| image_b64: str | |
| def verify_internal_secret(x_internal_secret: str = Header(None)): | |
| if x_internal_secret != INTERNAL_SECRET: | |
| raise HTTPException( | |
| status_code=status.HTTP_403_FORBIDDEN, | |
| detail="Forbidden: Invalid or missing X-Internal-Secret header" | |
| ) | |
| return x_internal_secret | |
| async def health_check(): | |
| if not is_model_ready: | |
| return JSONResponse(status_code=503, content={"status": "loading"}) | |
| return {"status": "ready"} | |
| def preprocess_image(image_b64: str): | |
| # Decode base64 | |
| if "," in image_b64: | |
| image_b64 = image_b64.split(",")[1] | |
| image_bytes = base64.b64decode(image_b64) | |
| image = Image.open(io.BytesIO(image_bytes)).convert("RGB") | |
| # Resize to 224x224 (ConvNeXt standard) | |
| image = image.resize((224, 224), Image.Resampling.BILINEAR) | |
| # Convert to numpy array and normalize | |
| # ConvNeXt ImageNet Mean: [0.485, 0.456, 0.406] | |
| # ConvNeXt ImageNet Std: [0.229, 0.224, 0.225] | |
| img_arr = np.array(image, dtype=np.float32) / 255.0 | |
| mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) | |
| std = np.array([0.229, 0.224, 0.225], dtype=np.float32) | |
| img_arr = (img_arr - mean) / std | |
| # HWC to CHW format | |
| img_arr = np.transpose(img_arr, (2, 0, 1)) | |
| # Add batch dimension | |
| img_arr = np.expand_dims(img_arr, axis=0) | |
| return img_arr | |
| def run_inference(img_arr: np.ndarray): | |
| input_name = ort_session.get_inputs()[0].name | |
| outputs = ort_session.run(None, {input_name: img_arr}) | |
| logits = outputs[0][0] | |
| # Apply temperature scaling | |
| scaled_logits = logits / temperature | |
| # Softmax | |
| exp_logits = np.exp(scaled_logits - np.max(scaled_logits)) | |
| probs = exp_logits / exp_logits.sum() | |
| pred_idx = int(np.argmax(probs)) | |
| confidence = float(probs[pred_idx]) | |
| label = class_labels[pred_idx] if pred_idx < len(class_labels) else f"class_{pred_idx}" | |
| return label, confidence | |
| async def infer(payload: InferenceRequest): | |
| if not is_model_ready: | |
| return JSONResponse(status_code=503, content={"status": "loading"}) | |
| try: | |
| # 1. Preprocess | |
| img_arr = await asyncio.to_thread(preprocess_image, payload.image_b64) | |
| # 2. Run Inference | |
| label, confidence = await asyncio.to_thread(run_inference, img_arr) | |
| # 3. Generate Heatmap (Mock for now, as True CAM requires PyTorch hooks) | |
| mock_heatmap = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" | |
| return { | |
| "raw_label": label, | |
| "confidence": confidence, | |
| "heatmap_b64": mock_heatmap | |
| } | |
| except Exception as e: | |
| print(f"Inference error: {e}") | |
| raise HTTPException(status_code=500, detail="Inference failed") | |