Spandan / app.py
TheDeepDas's picture
Upload app.py with huggingface_hub
8fa845f verified
Raw History Blame Contribute Delete
8.01 kB
import json
import os
import gradio as gr
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from joblib import load
from scipy.signal import find_peaks
from scipy.stats import entropy
try:
import spaces
except ImportError:
class _LocalSpaces:
@staticmethod
def GPU(function):
return function
spaces = _LocalSpaces()
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODEL_DIR = os.path.join(BASE_DIR, "best_models")
ARTIFACT_DIR = os.path.join(BASE_DIR, "artifacts")
DEVICE = torch.device("cpu")
WINDOW_SIZE = 60
PHYSICAL_LABELS = {0: "STRESS", 1: "PHYSICAL ACTIVITY", 2: "NORMAL"}
DISTRACTION_LABELS = {0: "FOCUSED", 1: "DISTRACTED"}
class WearableMLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(13, 256), nn.BatchNorm1d(256), nn.ReLU(), nn.Dropout(0.3),
nn.Linear(256, 128), nn.BatchNorm1d(128), nn.ReLU(), nn.Dropout(0.3),
nn.Linear(128, 64), nn.BatchNorm1d(64), nn.ReLU(), nn.Dropout(0.3),
nn.Linear(64, 3),
)
def forward(self, inputs):
return self.net(inputs)
class DistractionNet(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(7, 64), nn.BatchNorm1d(64), nn.ReLU(), nn.Dropout(0.3),
nn.Linear(64, 32), nn.BatchNorm1d(32), nn.ReLU(), nn.Dropout(0.2),
nn.Linear(32, 1), nn.Sigmoid(),
)
def forward(self, inputs):
return self.net(inputs).squeeze(1)
def load_model(model, filename):
path = os.path.join(MODEL_DIR, filename)
if not os.path.exists(path):
raise FileNotFoundError(f"Missing model file: {path}")
model.load_state_dict(torch.load(path, map_location=DEVICE))
model.eval()
return model
physical_model = load_model(WearableMLP(), "model_1c_state_scratch.pt")
distraction_model = load_model(DistractionNet(), "model_2c_distraction_scratch.pt")
physical_scaler = load(os.path.join(ARTIFACT_DIR, "physical_scaler.joblib"))
distraction_scaler = load(os.path.join(ARTIFACT_DIR, "distraction_scaler.joblib"))
def check_values(values):
values = np.asarray(values, dtype=np.float32)
if not np.isfinite(values).all():
raise gr.Error("All input values must be finite numbers.")
return values
@spaces.GPU
def predict_physical_state(
heart_rate, spo2, skin_temperature, accel_x, accel_y, accel_z,
gyro_x, gyro_y, gyro_z, lat_delta, lon_delta, altitude, time_delta,
):
values = check_values([
heart_rate, spo2, skin_temperature, accel_x, accel_y, accel_z,
gyro_x, gyro_y, gyro_z, lat_delta, lon_delta, altitude, time_delta,
]).reshape(1, -1)
scaled = physical_scaler.transform(values).astype(np.float32)
with torch.inference_mode():
probabilities = torch.softmax(
physical_model(torch.from_numpy(scaled)), dim=1
)[0].numpy()
class_id = int(np.argmax(probabilities))
return {
"model": "model_1c_state_scratch",
"task": "physical_state",
"class_id": class_id,
"state": PHYSICAL_LABELS[class_id],
"probability": round(float(probabilities[class_id]), 6),
"probabilities": {
PHYSICAL_LABELS[index]: round(float(value), 6)
for index, value in enumerate(probabilities)
},
"alert": class_id == 0,
}
def calculate_window_features(samples):
if isinstance(samples, dict):
samples = samples.get("samples")
if not isinstance(samples, list) or len(samples) != WINDOW_SIZE:
raise gr.Error("The distraction input must contain exactly 60 samples.")
frame = pd.DataFrame(samples)
required = [
"heart_rate", "accel_x", "accel_y", "accel_z",
"gyro_x", "gyro_y", "gyro_z", "lat_delta", "lon_delta", "time_delta",
]
missing = [name for name in required if name not in frame.columns]
if missing:
raise gr.Error(f"Missing sample fields: {', '.join(missing)}")
frame = frame[required].astype(np.float32)
if not np.isfinite(frame.to_numpy()).all():
raise gr.Error("All sample values must be finite numbers.")
acceleration = np.sqrt(
frame["accel_x"] ** 2 + frame["accel_y"] ** 2 + frame["accel_z"] ** 2
).to_numpy()
gyroscope = np.sqrt(
frame["gyro_x"] ** 2 + frame["gyro_y"] ** 2 + frame["gyro_z"] ** 2
).to_numpy()
displacement = np.sqrt(frame["lat_delta"] ** 2 + frame["lon_delta"] ** 2)
speed = np.mean(displacement / (frame["time_delta"] + 1e-6))
histogram, _ = np.histogram(acceleration, bins=10, density=True)
histogram += 1e-8
return np.asarray([
np.var(acceleration),
np.var(gyroscope),
entropy(histogram),
len(find_peaks(acceleration)[0]),
np.var(frame["heart_rate"].to_numpy()),
(frame["heart_rate"].iloc[-1] - frame["heart_rate"].iloc[0]) / len(frame),
speed,
], dtype=np.float32)
@spaces.GPU
def predict_distraction(samples_json):
try:
samples = json.loads(samples_json)
except (TypeError, json.JSONDecodeError) as error:
raise gr.Error("Enter valid JSON containing a samples array.") from error
features = calculate_window_features(samples).reshape(1, -1)
scaled = distraction_scaler.transform(features).astype(np.float32)
with torch.inference_mode():
probability = float(distraction_model(torch.from_numpy(scaled))[0].item())
class_id = int(probability >= 0.5)
return {
"model": "model_2c_distraction_scratch",
"task": "focus_distraction",
"class_id": class_id,
"state": DISTRACTION_LABELS[class_id],
"probability": round(probability, 6),
"alert": class_id == 1,
"window_size": WINDOW_SIZE,
}
physical_inputs = [
gr.Number(label="Heart rate", value=80),
gr.Number(label="SpO2", value=97),
gr.Number(label="Skin temperature", value=36.5),
gr.Number(label="Accel X", value=0),
gr.Number(label="Accel Y", value=0),
gr.Number(label="Accel Z", value=9.81),
gr.Number(label="Gyro X", value=0),
gr.Number(label="Gyro Y", value=0),
gr.Number(label="Gyro Z", value=0),
gr.Number(label="Latitude delta", value=0),
gr.Number(label="Longitude delta", value=0),
gr.Number(label="Altitude", value=20),
gr.Number(label="Time delta", value=30),
]
with gr.Blocks(title="Spandan Smartwatch AI") as demo:
gr.Markdown("# Spandan Smartwatch AI\nTwo-model inference for physical state and focus monitoring.")
with gr.Tab("Physical state"):
gr.Markdown("Enter one cleaned telemetry row. The endpoint returns STRESS, PHYSICAL ACTIVITY, or NORMAL.")
physical_button = gr.Button("Run physical-state model", variant="primary")
physical_output = gr.JSON(label="Physical-state output")
physical_button.click(
predict_physical_state,
inputs=physical_inputs,
outputs=physical_output,
api_name="predict_physical_state",
)
with gr.Tab("Focus and distraction"):
gr.Markdown("Paste JSON with exactly 60 ordered samples from one officer.")
distraction_input = gr.Textbox(
label="60-sample JSON",
lines=12,
placeholder='{"samples": [{"heart_rate": 80, "accel_x": 0, ...}]}'
)
distraction_button = gr.Button("Run distraction model", variant="primary")
distraction_output = gr.JSON(label="Distraction output")
distraction_button.click(
predict_distraction,
inputs=distraction_input,
outputs=distraction_output,
api_name="predict_distraction",
)
if __name__ == "__main__":
demo.launch()