# app.py from fastapi import FastAPI, Response, UploadFile, File, Body, HTTPException from pydantic import BaseModel from typing import List from fastapi.responses import JSONResponse import numpy as np from PIL import Image import io import tensorflow as tf import time app = FastAPI() # ==== Schemas (show up in Swagger) ==== class PredictOut(BaseModel): result: List[int] model_config = {"json_schema_extra": {"examples": [{"result": [0,1,2,3]}]}} class StatusOut(BaseModel): status: str message: str class AcceptSizeOut(BaseModel): status: str accept_size: List[int] class LoadModelIn(BaseModel): path: str name: str class ActivateModelIn(BaseModel): name: str # ==== Service ==== class Service: def __init__(self): self.model = None self.models = {} self.active_model = "default" def activate_model(self, name): self.model = self.models[name] self.active_model = name return 0 def load_model(self, path, name): if name in self.models: # Model already loaded, skip loading return 0 self.models[name] = tf.keras.models.load_model(path) return 0 def get_accept_size(self): print(self.model.input_shape) return list(self.model.input_shape) # (B,H,W,C) def get_output_size(self): return list(self.model.output_shape) # (B,H,W,C) or (B,H,W) def predict(self, data: bytes) -> List[int]: target_size = self.get_accept_size() image = np.reshape( np.frombuffer(data, dtype=np.uint8), (target_size[1], target_size[2], target_size[3]) ) / 255.0 predictions = self.model.predict(np.array([image])) if predictions[0].ndim == 2: classed = predictions[0] else: classed = tf.argmax(predictions[0], axis=2) return classed.numpy().reshape(-1).tolist() def predict_raw(self, data: bytes) -> np.ndarray: target_size = self.get_accept_size() image = np.reshape( np.frombuffer(data, dtype=np.uint8), (target_size[1], target_size[2], target_size[3]) ) / 255.0 predictions = self.model({"input_layer": np.array([image], dtype=np.float32)}).numpy() if predictions[0].ndim == 2: classed = predictions[0][:, :, np.newaxis].clip(0, 255) return classed.astype(np.uint8).reshape(-1).tolist() else: classed = predictions[0][:, :, :] res = (classed * 255).astype(np.uint8).reshape(-1) return np.asarray(res) svc = Service() def _prep_raw_bytes_for_predict(buf: bytes) -> bytes: """Decode JPEG/PNG -> RGB, resize to model size, return raw uint8 bytes HxWxC.""" h, w, c = svc.get_accept_size()[1:4] img = Image.open(io.BytesIO(buf)).convert("RGB") if img.size != (w, h): img = img.resize((w, h), Image.BILINEAR) arr = np.asarray(img, dtype=np.uint8) # HxWx3 uint8 return arr.tobytes() # ==== Routes ==== @app.post("/predict", response_model=bytes, summary="Predict (classified indices)") async def predict(file: UploadFile = File(...)): try: buf = await file.read() raw = _prep_raw_bytes_for_predict(buf) result = svc.predict(raw) return Response(content=result, media_type="application/octet-stream") except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/predictRaw", response_model=bytes, summary="Predict (raw/uint8 flattened)") async def predict_raw(file: UploadFile = File(...)): try: start_time = time.time() buf = await file.read() result = svc.predict_raw(buf) elapsed_time = time.time() - start_time print(f"Prediction took {elapsed_time:.2f} seconds") return Response(content=result.tobytes(), media_type="application/octet-stream") except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/loadModel", response_model=StatusOut, summary="Load a model from path") async def load_model(payload: LoadModelIn): try: svc.load_model(payload.path, payload.name) return StatusOut(status="success", message=f"Model {payload.name} loaded successfully.") except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/activateModel", response_model=StatusOut, summary="Activate a loaded model") async def activate_model(payload: ActivateModelIn): try: svc.activate_model(payload.name) return StatusOut(status="success", message=f"Model {payload.name} activated successfully.") except KeyError: raise HTTPException(status_code=404, detail=f"Model {payload.name} not found.") except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/getAcceptSize", response_model=AcceptSizeOut, summary="Get input tensor shape") async def get_accept_size(): try: size = svc.get_accept_size()[1:] # Exclude batch size return AcceptSizeOut(status="success", accept_size=size) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/getOutputSize", response_model=AcceptSizeOut, summary="Get output tensor shape") async def get_output_size(): try: size = svc.get_output_size()[1:] return AcceptSizeOut(status="success", accept_size=size) except Exception as e: raise HTTPException(status_code=500, detail=str(e))