SF_FastAPI / app.py
COCODEDE04's picture
Update app.py
b8edb00 verified
raw
history blame
3.4 kB
import json
import os
from typing import Any, Dict
import numpy as np
import tensorflow as tf
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
# ----------------- CONFIG -----------------
MODEL_PATH = os.getenv("MODEL_PATH", "best_model.h5")
STATS_PATH = os.getenv("STATS_PATH", "means_std.json")
CLASSES = ["Top", "Mid-Top", "Mid", "Mid-Low", "Low"]
# ------------------------------------------
print("Loading model and stats...")
model = tf.keras.models.load_model(MODEL_PATH, compile=False)
with open(STATS_PATH, "r") as f:
stats: Dict[str, Dict[str, float]] = json.load(f)
FEATURES = list(stats.keys())
print("Feature order:", FEATURES)
def _z(val: Any, mean: float, sd: float) -> float:
try:
v = float(val)
except Exception:
return 0.0
if not sd:
return 0.0
return (v - mean) / sd
def coral_probs_from_logits(logits_np: np.ndarray) -> np.ndarray:
"""(N, K-1) logits -> (N, K) probabilities for CORAL ordinal output."""
logits = tf.convert_to_tensor(logits_np, dtype=tf.float32)
sig = tf.math.sigmoid(logits) # (N, K-1)
left = tf.concat([tf.ones_like(sig[:, :1]), sig], axis=1)
right = tf.concat([sig, tf.zeros_like(sig[:, :1])], axis=1)
probs = tf.clip_by_value(left - right, 1e-12, 1.0)
return probs.numpy()
# ------------- FastAPI app ----------------
app = FastAPI(title="Static Fingerprint API", version="1.0.0")
# Allow Excel / local tools to call the API
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def root():
return {
"message": "Static Fingerprint API is running.",
"try": ["GET /health", "POST /predict"],
}
@app.get("/health")
def health():
return {
"status": "ok",
"features": FEATURES,
"classes": CLASSES,
"model_file": MODEL_PATH,
"stats_file": STATS_PATH,
}
@app.post("/predict")
async def predict(req: Request):
"""
Body: a single JSON dict mapping feature -> numeric value.
Example:
{
"autosuf_oper": 1.0,
"cov_improductiva": 0.9,
...
}
"""
payload = await req.json()
if not isinstance(payload, dict):
return {"error": "Expected a JSON object mapping feature -> value."}
# Build z-scores in strict model order
z = []
z_detail = {}
missing = []
for f in FEATURES:
val = payload.get(f, 0)
mean = stats[f]["mean"]
sd = stats[f]["std"]
zf = _z(val, mean, sd)
z.append(zf)
z_detail[f] = zf
if f not in payload:
missing.append(f)
X = np.array([z], dtype=np.float32)
raw = model.predict(X, verbose=0)
# Detect CORAL (K-1) vs softmax (K)
if raw.ndim == 2 and raw.shape[1] == (len(CLASSES) - 1):
probs = coral_probs_from_logits(raw)[0]
else:
probs = raw[0]
# If it's not normalized, normalize defensively:
s = float(np.sum(probs))
if s > 0:
probs = probs / s
pred_idx = int(np.argmax(probs))
return {
"input_ok": (len(missing) == 0),
"missing": missing,
"z_scores": z_detail,
"probabilities": {CLASSES[i]: float(probs[i]) for i in range(len(CLASSES))},
"predicted_state": CLASSES[pred_idx],
}