Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import numpy as np | |
| import gradio as gr | |
| import tensorflow as tf | |
| # ---------- CONFIG ---------- | |
| MODEL_PATH = "best_model.h5" # <- you converted & uploaded this | |
| STATS_PATH = "Means & Std for Excel.json" # <- keep exact filename | |
| CLASSES = ["Top", "Mid-Top", "Mid", "Mid-Low", "Low"] # your group order | |
| # ---------------------------- | |
| print("Loading model and stats...") | |
| model = tf.keras.models.load_model(MODEL_PATH, compile=False) | |
| with open(STATS_PATH, "r") as f: | |
| stats = json.load(f) | |
| # Feature order: use the order from JSON to remain consistent | |
| FEATURES = list(stats.keys()) | |
| print("Feature order:", FEATURES) | |
| def _zscore(val: float, mean: float, sd: float) -> float: | |
| # Robust z-score (avoid division by zero) | |
| try: | |
| v = float(val) | |
| except (TypeError, ValueError): | |
| v = 0.0 | |
| return 0.0 if (sd is None or sd == 0) else (v - mean) / sd | |
| def predict_core(ratios: dict): | |
| """ | |
| ratios: dict mapping feature name -> raw numeric ratio. | |
| Returns a dict with predicted_state, probabilities, z_scores, missing. | |
| """ | |
| # Validate presence | |
| missing = [f for f in FEATURES if f not in ratios] | |
| # Build z-score vector in the exact FEATURE order | |
| zscores = [] | |
| zscores_dict = {} | |
| for f in FEATURES: | |
| mean = stats[f]["mean"] | |
| sd = stats[f]["std"] | |
| val = ratios.get(f, 0.0) | |
| z = _zscore(val, mean, sd) | |
| zscores.append(z) | |
| zscores_dict[f] = z | |
| X = np.array([zscores], dtype=np.float32) | |
| probs = model.predict(X, verbose=0)[0] | |
| pred_idx = int(np.argmax(probs)) | |
| pred_state = CLASSES[pred_idx] | |
| return { | |
| "input_ok": len(missing) == 0, | |
| "missing": missing, | |
| "z_scores": zscores_dict, | |
| "probabilities": {CLASSES[i]: float(probs[i]) for i in range(len(CLASSES))}, | |
| "predicted_state": pred_state | |
| } | |
| # ---------- Gradio adapter ---------- | |
| def predict_from_json(payload, x_api_key: str = ""): | |
| """ | |
| Gradio will pass the JSON from the UI or /run/predict. | |
| We accept either: | |
| { ...feature: value... } | |
| or [ { ...feature: value... } ] (unwrap common API client shape) | |
| """ | |
| # Optional simple API key | |
| if API_KEY and x_api_key.strip() != API_KEY: | |
| return {"error": "Unauthorized: missing or invalid X-API-Key header"} | |
| # Unwrap if list-of-one was sent | |
| if isinstance(payload, list) and len(payload) == 1 and isinstance(payload[0], dict): | |
| payload = payload[0] | |
| if not isinstance(payload, dict): | |
| return {"error": "Invalid payload: expected a JSON object mapping feature -> value."} | |
| return predict_core(payload) | |
| # Minimal UI: a single JSON box for quick manual tests. | |
| iface = gr.Interface( | |
| fn=predict_from_json, | |
| inputs=gr.JSON(label="ratios JSON (dict of feature -> value)"), | |
| outputs="json", | |
| title="Static Fingerprint Model API", | |
| description=( | |
| "POST JSON to /run/predict with a dict of your 21 ratios. " | |
| "Server normalises using saved means/stds and returns probabilities + state." | |
| ) | |
| ) | |
| if __name__ == "__main__": | |
| iface.launch() |