Spaces:
Sleeping
Sleeping
| 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=["*"], | |
| ) | |
| def root(): | |
| return { | |
| "message": "Static Fingerprint API is running.", | |
| "try": ["GET /health", "POST /predict"], | |
| } | |
| def health(): | |
| return { | |
| "status": "ok", | |
| "features": FEATURES, | |
| "classes": CLASSES, | |
| "model_file": MODEL_PATH, | |
| "stats_file": STATS_PATH, | |
| } | |
| 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], | |
| } |