File size: 3,176 Bytes
094fd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import os
import json
import numpy as np
import tensorflow as tf
import gradio as gr

# ---------- CONFIG ----------
MODEL_PATH = "best_model.h5"                  # or best_model.keras
STATS_PATH = "Means & Std for Excel.json"     # must match filename in repo
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 = json.load(f)

FEATURES = list(stats.keys())
print("Feature order:", FEATURES)


# ---------- Utility helpers ----------
def _zscore(val: float, mean: float, sd: float) -> float:
    """Compute safe z-score (handles NaNs and zeros)."""
    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 coral_probs_from_logits(logits_np):
    """Convert (N, K-1) CORAL logits → (N, K) probabilities."""
    logits = tf.convert_to_tensor(logits_np, dtype=tf.float32)
    sig = tf.math.sigmoid(logits)
    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()


def predict_core(ratios: dict):
    """
    ratios: dict mapping feature name -> raw numeric ratio.
    Returns dict with predicted_state, probabilities, z_scores, missing.
    """
    missing = [f for f in FEATURES if f not in ratios]

    # Build z-score vector in same 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)
    logits = model.predict(X, verbose=0)
    probs = coral_probs_from_logits(logits)[0]  # now 5 probabilities

    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 interface ----------
def predict_from_json(payload, x_api_key: str = ""):
    """
    Accepts either:
        {feature: value}
      or [{feature: value}]
    """
    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)


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 + predicted state."
    ),
)

if __name__ == "__main__":
    iface.launch()