""" app.py — Property Classifier FastAPI inference server with embedded web UI. No gradio = no dependency conflicts on HuggingFace Spaces. """ import os import io import json import time import numpy as np from PIL import Image from fastapi import FastAPI, File, UploadFile, HTTPException from fastapi.responses import HTMLResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0" import tensorflow as tf # ── Paths ───────────────────────────────────────────────────────────────── MODEL_PATH = "models/building_classifier.h5" TOKENIZER_PATH = "models/tokenizer.json" IMG_SIZE = (224, 224) # ── Class display names ─────────────────────────────────────────────────── CLASS_DISPLAY = { "exterior_facade": "Exterior Facade", "office_interior": "Office Interior", "warehouse": "Warehouse", "hvac_pipeline": "HVAC Pipeline", } CLASS_COLORS = { "exterior_facade": "#3b82f6", "office_interior": "#22c55e", "warehouse": "#f59e0b", "hvac_pipeline": "#a855f7", } # ── Load model once at startup ──────────────────────────────────────────── model: tf.keras.Model | None = None classes: list[str] = [] try: if os.path.exists(MODEL_PATH) and os.path.exists(TOKENIZER_PATH): print("Loading model...") model = tf.keras.models.load_model(MODEL_PATH) tok = json.loads(open(TOKENIZER_PATH, encoding="utf-8").read()) classes = tok["class_names"] print(f"Model loaded successfully. Classes: {classes}") else: print(f"WARNING: Model not found at {MODEL_PATH}") except Exception as e: print(f"ERROR loading model: {e}") # ── FastAPI app ─────────────────────────────────────────────────────────── app = FastAPI(title="Property Classifier", version="2.0.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) # ── Embedded HTML UI ────────────────────────────────────────────────────── HTML = """ Property Classifier
Property Classifier
ResNet101V2 Online
Image Input
▶
Drop a building image here
JPEG or PNG
Prediction History
No predictions yet
Classification Result
▤
Awaiting image
Upload a property image to begin
---
Confidence: 0%  •  Latency: 0ms
Probability Distribution
""" @app.get("/", response_class=HTMLResponse) async def root(): return HTML @app.get("/health") async def health(): return JSONResponse({ "status": "ok", "model_loaded": model is not None, "classes": classes, "input_size": list(IMG_SIZE), }) @app.post("/predict") async def predict(file: UploadFile = File(...)): # Validate if file.content_type not in ("image/jpeg", "image/png", "image/jpg"): raise HTTPException(415, f"Unsupported file type: {file.content_type}") if model is None: raise HTTPException(503, "Model not loaded. Check Space logs.") # Read image data = await file.read() if len(data) == 0: raise HTTPException(400, "Empty file received.") try: img = Image.open(io.BytesIO(data)).convert("RGB") img = img.resize(IMG_SIZE, Image.BILINEAR) except Exception as e: raise HTTPException(400, f"Cannot read image: {e}") # Inference arr = np.array(img, dtype=np.float32) / 255.0 arr = np.expand_dims(arr, 0) t0 = time.time() probs = model.predict(arr, verbose=0)[0] ms = round((time.time() - t0) * 1000, 1) # Build response top = np.argsort(probs)[::-1] return JSONResponse({ "top_class": CLASS_DISPLAY.get(classes[top[0]], classes[top[0]]), "confidence": float(probs[top[0]]), "latency_ms": ms, "predictions": [ { "class": CLASS_DISPLAY.get(classes[i], classes[i]), "class_raw": classes[i], "probability": float(probs[i]), } for i in top[:4] ], })