techavenger123
Project Structure File.
2f37dc9
"""
FaultSense — LightGBM Fault Prediction App
Fixes applied:
1. Model loads at module level so gunicorn workers pick it up
2. Select dropdown works cross-platform
"""
import os
import warnings
import numpy as np
import pandas as pd
warnings.filterwarnings("ignore")
from sklearn.model_selection import train_test_split
from sklearn.metrics import (
roc_auc_score, accuracy_score, precision_score,
recall_score, f1_score, log_loss, confusion_matrix
)
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
import joblib
from lightgbm import LGBMClassifier
from flask import Flask, request, jsonify, render_template_string
# ─────────────────────────────────────────────
# CONFIG
# ─────────────────────────────────────────────
DATA_PATH = "synthetic_nim_parallel_10000.csv"
MODEL_PATH = "/tmp/faultsense_model.joblib"
DROP_COLS = ["location"]
TARGET = "faulty"
CAT_COLS = ["equipment"]
NUM_COLS = ["temperature", "pressure", "vibration", "humidity"]
RANDOM_STATE = 42
THRESHOLD = 0.5
FIXED_PARAMS = dict(
max_depth=8,
num_leaves=50,
min_child_samples=20,
subsample=0.8,
colsample_bytree=0.8,
class_weight="balanced",
random_state=RANDOM_STATE,
verbose=-1,
)
BEST_CONFIG = {
"learning_rate": 0.05,
"n_estimators": 165,
"train_ratio": 0.80,
"val_ratio": 0.10,
"test_ratio": 0.10,
}
EQUIPMENT_OPTIONS = ["pump", "compressor", "motor", "valve", "sensor"]
# ─────────────────────────────────────────────
# MODEL TRAINING / LOADING
# ─────────────────────────────────────────────
def make_preprocessor():
return ColumnTransformer([
("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), CAT_COLS),
("num", "passthrough", NUM_COLS),
])
def train_model(cfg: dict):
print(f"Training: lr={cfg['learning_rate']}, n_est={cfg['n_estimators']}")
df_raw = pd.read_csv(DATA_PATH)
df_raw = df_raw.drop(columns=DROP_COLS, errors="ignore")
X = df_raw.drop(columns=[TARGET])
y = df_raw[TARGET]
train_r, val_r, test_r = cfg["train_ratio"], cfg["val_ratio"], cfg["test_ratio"]
X_trainval, X_test, y_trainval, y_test = train_test_split(
X, y, test_size=test_r, stratify=y, random_state=RANDOM_STATE
)
val_relative = val_r / (train_r + val_r)
X_train, X_val, y_train, y_val = train_test_split(
X_trainval, y_trainval, test_size=val_relative,
stratify=y_trainval, random_state=RANDOM_STATE
)
pipeline = Pipeline([
("pre", make_preprocessor()),
("clf", LGBMClassifier(
n_estimators=cfg["n_estimators"],
learning_rate=cfg["learning_rate"],
**FIXED_PARAMS
))
])
pipeline.fit(X_train, y_train)
y_prob = pipeline.predict_proba(X_test)[:, 1]
y_pred = (y_prob >= THRESHOLD).astype(int)
test_metrics = {
"test_auc": round(roc_auc_score(y_test, y_prob), 4),
"test_accuracy": round(accuracy_score(y_test, y_pred), 4),
"test_precision": round(precision_score(y_test, y_pred, zero_division=0), 4),
"test_recall": round(recall_score(y_test, y_pred, zero_division=0), 4),
"test_f1": round(f1_score(y_test, y_pred, zero_division=0), 4),
"test_logloss": round(log_loss(y_test, y_prob), 4),
}
cm = confusion_matrix(y_test, y_pred).tolist()
artifact = {"pipeline": pipeline, "config": cfg, "test_metrics": test_metrics, "cm": cm}
print(f"Model saved → {MODEL_PATH} AUC={test_metrics['test_auc']} F1={test_metrics['test_f1']}")
return artifact
def load_or_train():
if os.path.exists(MODEL_PATH):
print(f"Loading saved model from {MODEL_PATH}")
return joblib.load(MODEL_PATH)
return train_model(BEST_CONFIG)
# ─────────────────────────────────────────────
# LOAD MODEL AT MODULE LEVEL (runs under gunicorn)
# ─────────────────────────────────────────────
ARTIFACT = load_or_train()
# ─────────────────────────────────────────────
# FLASK APP
# ─────────────────────────────────────────────
app = Flask(__name__)
HTML = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FaultSense — Equipment Fault Predictor</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;500;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0c10; --surface: #111318; --surface2: #181c24;
--border: #232838; --accent: #00e5a0; --accent2: #ff4d6d;
--accent3: #4d9fff; --text: #e8eaf0; --muted: #6b7280;
--mono: 'Space Mono', monospace; --sans: 'DM Sans', sans-serif;
}
html { font-size: 16px; }
body { background: var(--bg); color: var(--text); font-family: var(--sans); min-height: 100vh; overflow-x: hidden; }
body::before {
content: ''; position: fixed; inset: 0;
background-image: linear-gradient(rgba(0,229,160,.04) 1px, transparent 1px), linear-gradient(90deg, rgba(0,229,160,.04) 1px, transparent 1px);
background-size: 40px 40px; pointer-events: none; z-index: 0;
}
.blob { position: fixed; width: 600px; height: 600px; border-radius: 50%; filter: blur(120px); opacity: .15; pointer-events: none; animation: drift 12s ease-in-out infinite alternate; z-index: 0; }
.blob-1 { background: var(--accent); top: -200px; left: -200px; }
.blob-2 { background: var(--accent3); bottom: -200px; right: -100px; animation-delay: -6s; }
@keyframes drift { from { transform: translate(0,0) scale(1); } to { transform: translate(40px,30px) scale(1.05); } }
.wrapper { position: relative; z-index: 1; max-width: 1100px; margin: 0 auto; padding: 40px 24px 80px; }
header { display: flex; align-items: center; gap: 16px; margin-bottom: 48px; border-bottom: 1px solid var(--border); padding-bottom: 24px; }
.logo-mark { width: 44px; height: 44px; background: var(--accent); border-radius: 10px; display: grid; place-items: center; font-family: var(--mono); font-weight: 700; font-size: 18px; color: var(--bg); flex-shrink: 0; }
header h1 { font-family: var(--mono); font-size: 1.5rem; letter-spacing: -.5px; }
header p { font-size: .85rem; color: var(--muted); margin-top: 2px; }
.badge { margin-left: auto; font-family: var(--mono); font-size: .7rem; background: rgba(0,229,160,.12); color: var(--accent); border: 1px solid rgba(0,229,160,.3); border-radius: 6px; padding: 4px 10px; white-space: nowrap; }
.main-grid { display: grid; grid-template-columns: 1fr 380px; gap: 24px; align-items: start; }
@media (max-width: 860px) { .main-grid { grid-template-columns: 1fr; } }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 28px; }
.card-title { font-family: var(--mono); font-size: .75rem; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); margin-bottom: 20px; display: flex; align-items: center; gap: 8px; }
.card-title::before { content: ''; display: inline-block; width: 6px; height: 6px; background: var(--accent); border-radius: 50%; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 560px) { .form-grid { grid-template-columns: 1fr; } }
.field { display: flex; flex-direction: column; gap: 8px; }
.field label { font-size: .78rem; font-family: var(--mono); color: var(--muted); letter-spacing: .5px; }
/* CUSTOM DROPDOWN */
.custom-select { position: relative; width: 100%; }
.cs-trigger {
background: var(--surface2); border: 1px solid var(--border);
border-radius: 10px; color: var(--text); font-family: var(--mono);
font-size: .9rem; padding: 11px 14px; width: 100%; cursor: pointer;
display: flex; justify-content: space-between; align-items: center;
transition: border-color .2s;
}
.cs-trigger:hover, .cs-trigger.open { border-color: var(--accent); }
.cs-arrow { font-size: 10px; color: var(--muted); transition: transform .2s; }
.cs-trigger.open .cs-arrow { transform: rotate(180deg); }
.cs-options {
position: absolute; top: calc(100% + 4px); left: 0; right: 0;
background: #1a1e2a; border: 1px solid var(--accent);
border-radius: 10px; z-index: 999; overflow: hidden;
display: none; flex-direction: column;
}
.cs-options.open { display: flex; }
.cs-option {
padding: 11px 14px; font-family: var(--mono); font-size: .9rem;
color: var(--text); cursor: pointer; transition: background .15s;
}
.cs-option:hover { background: rgba(0,229,160,.15); color: var(--accent); }
.cs-option.selected { color: var(--accent); background: rgba(0,229,160,.08); }
.slider-wrap { display: flex; flex-direction: column; gap: 6px; }
.slider-row { display: flex; align-items: center; gap: 10px; }
input[type=range] { flex: 1; -webkit-appearance: none; height: 4px; border-radius: 4px; background: var(--border); outline: none; cursor: pointer; }
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--accent); transition: transform .15s; }
input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.3); }
input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--accent); border: none; }
.slider-val { font-family: var(--mono); font-size: .85rem; color: var(--accent); min-width: 60px; text-align: right; }
.btn-predict { margin-top: 24px; width: 100%; padding: 14px; background: var(--accent); color: var(--bg); border: none; border-radius: 12px; font-family: var(--mono); font-size: 1rem; font-weight: 700; letter-spacing: 1px; cursor: pointer; transition: transform .15s, box-shadow .2s; }
.btn-predict:hover { transform: translateY(-2px); box-shadow: 0 0 32px rgba(0,229,160,.5); }
.btn-predict:disabled { background: var(--muted); cursor: not-allowed; transform: none; }
.result-card { border-radius: 16px; padding: 28px; border: 1px solid var(--border); background: var(--surface); transition: border-color .4s; }
.result-card.faulty { border-color: var(--accent2); background: rgba(255,77,109,.06); }
.result-card.healthy { border-color: var(--accent); background: rgba(0,229,160,.06); }
.verdict { font-family: var(--mono); font-size: 2rem; font-weight: 700; letter-spacing: -1px; margin-bottom: 6px; }
.verdict.faulty { color: var(--accent2); }
.verdict.healthy { color: var(--accent); }
.verdict-sub { font-size: .85rem; color: var(--muted); margin-bottom: 24px; }
.prob-bar-wrap { margin-bottom: 24px; }
.prob-label { font-family: var(--mono); font-size: .72rem; color: var(--muted); margin-bottom: 6px; display: flex; justify-content: space-between; }
.prob-track { height: 10px; background: var(--border); border-radius: 10px; overflow: hidden; }
.prob-fill { height: 100%; border-radius: 10px; transition: width .6s cubic-bezier(.4,0,.2,1); }
.prob-fill.faulty { background: linear-gradient(90deg, #ff4d6d, #ff8fa3); }
.prob-fill.healthy { background: linear-gradient(90deg, #00e5a0, #5eead4); }
.mini-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.mini-metric { background: var(--surface2); border-radius: 10px; padding: 12px; border: 1px solid var(--border); }
.mini-metric .mm-val { font-family: var(--mono); font-size: 1.1rem; font-weight: 700; color: var(--accent3); }
.mini-metric .mm-key { font-size: .7rem; color: var(--muted); margin-top: 2px; font-family: var(--mono); }
.info-row { display: flex; justify-content: space-between; align-items: center; padding: 9px 0; border-bottom: 1px solid var(--border); font-size: .82rem; }
.info-row:last-child { border-bottom: none; }
.info-key { color: var(--muted); font-family: var(--mono); font-size: .72rem; }
.info-val { font-family: var(--mono); color: var(--text); font-weight: 700; }
.info-val.green { color: var(--accent); }
.history-list { max-height: 260px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
.hist-item { background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; display: flex; justify-content: space-between; align-items: center; font-size: .78rem; }
.hist-equip { color: var(--muted); font-family: var(--mono); font-size: .7rem; }
.hist-badge { font-family: var(--mono); font-size: .68rem; padding: 3px 8px; border-radius: 6px; font-weight: 700; }
.hist-badge.faulty { background: rgba(255,77,109,.2); color: var(--accent2); }
.hist-badge.healthy { background: rgba(0,229,160,.2); color: var(--accent); }
.spinner { display: none; width: 20px; height: 20px; border: 2px solid rgba(10,12,16,.3); border-top-color: var(--bg); border-radius: 50%; animation: spin .6s linear infinite; margin: 0 auto; }
@keyframes spin { to { transform: rotate(360deg); } }
.btn-predict.loading .btn-text { display: none; }
.btn-predict.loading .spinner { display: block; }
.idle-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 32px 0; color: var(--muted); text-align: center; }
.idle-icon { font-size: 2.5rem; opacity: .4; }
.toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface2); border: 1px solid var(--accent2); color: var(--accent2); border-radius: 10px; padding: 12px 18px; font-family: var(--mono); font-size: .8rem; transform: translateY(80px); opacity: 0; transition: all .3s; z-index: 999; }
.toast.show { transform: translateY(0); opacity: 1; }
</style>
</head>
<body>
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="wrapper">
<header>
<div class="logo-mark">FS</div>
<div>
<h1>FaultSense</h1>
<p>LightGBM Equipment Fault Predictor</p>
</div>
<div class="badge" id="model-badge">Model Loading…</div>
</header>
<div class="main-grid">
<div style="display:flex;flex-direction:column;gap:20px;">
<div class="card">
<div class="card-title">Sensor Readings</div>
<div class="form-grid">
<div class="field" style="grid-column:1/-1">
<label>Equipment Type</label>
<div class="custom-select" id="equipment-wrapper">
<div class="cs-trigger" id="cs-trigger" onclick="toggleDropdown()">
<span id="cs-selected">Pump</span>
<span class="cs-arrow">▼</span>
</div>
<div class="cs-options" id="cs-options">
<div class="cs-option selected" onclick="selectOption('pump','Pump')">Pump</div>
<div class="cs-option" onclick="selectOption('compressor','Compressor')">Compressor</div>
<div class="cs-option" onclick="selectOption('motor','Motor')">Motor</div>
<div class="cs-option" onclick="selectOption('valve','Valve')">Valve</div>
<div class="cs-option" onclick="selectOption('sensor','Sensor')">Sensor</div>
</div>
</div>
<input type="hidden" id="equipment" value="pump">
</div>
<div class="field slider-wrap">
<label>Temperature (°C)</label>
<div class="slider-row">
<input type="range" id="temperature" min="-20" max="120" step="0.5" value="40"
oninput="document.getElementById('temperature-val').textContent=parseFloat(this.value).toFixed(1)+'°C'">
<span class="slider-val" id="temperature-val">40.0°C</span>
</div>
</div>
<div class="field slider-wrap">
<label>Pressure (bar)</label>
<div class="slider-row">
<input type="range" id="pressure" min="0" max="20" step="0.1" value="5"
oninput="document.getElementById('pressure-val').textContent=parseFloat(this.value).toFixed(1)+' bar'">
<span class="slider-val" id="pressure-val">5.0 bar</span>
</div>
</div>
<div class="field slider-wrap">
<label>Vibration (mm/s)</label>
<div class="slider-row">
<input type="range" id="vibration" min="0" max="50" step="0.1" value="5"
oninput="document.getElementById('vibration-val').textContent=parseFloat(this.value).toFixed(1)+' mm/s'">
<span class="slider-val" id="vibration-val">5.0 mm/s</span>
</div>
</div>
<div class="field slider-wrap">
<label>Humidity (%)</label>
<div class="slider-row">
<input type="range" id="humidity" min="0" max="100" step="1" value="50"
oninput="document.getElementById('humidity-val').textContent=parseInt(this.value)+'%'">
<span class="slider-val" id="humidity-val">50%</span>
</div>
</div>
</div>
<button class="btn-predict" id="predict-btn" onclick="runPredict()">
<span class="btn-text">⚡ Run Prediction</span>
<div class="spinner"></div>
</button>
</div>
<div class="card">
<div class="card-title">Prediction History</div>
<div class="history-list" id="history-list">
<div style="color:var(--muted);font-size:.8rem;font-family:var(--mono);text-align:center;padding:16px 0;">No predictions yet</div>
</div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:20px;">
<div class="result-card" id="result-card">
<div class="idle-state" id="idle-state">
<div class="idle-icon">🔬</div>
<p>Enter sensor readings<br>and run a prediction<br>to see results here.</p>
</div>
<div id="result-content" style="display:none;">
<div class="verdict" id="verdict-text"></div>
<div class="verdict-sub" id="verdict-sub"></div>
<div class="prob-bar-wrap">
<div class="prob-label"><span>Fault Probability</span><span id="prob-pct"></span></div>
<div class="prob-track"><div class="prob-fill" id="prob-fill" style="width:0%"></div></div>
</div>
<div class="mini-metrics" id="mini-metrics"></div>
</div>
</div>
<div class="card">
<div class="card-title">Model Configuration</div>
<div id="model-info">
<div style="color:var(--muted);font-size:.8rem;font-family:var(--mono);text-align:center;padding:12px 0;">Loading…</div>
</div>
</div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
let csOpen = false;
function toggleDropdown() {
csOpen = !csOpen;
document.getElementById('cs-trigger').classList.toggle('open', csOpen);
document.getElementById('cs-options').classList.toggle('open', csOpen);
}
function selectOption(value, label) {
document.getElementById('equipment').value = value;
document.getElementById('cs-selected').textContent = label;
document.querySelectorAll('.cs-option').forEach(o => o.classList.remove('selected'));
event.target.classList.add('selected');
csOpen = false;
document.getElementById('cs-trigger').classList.remove('open');
document.getElementById('cs-options').classList.remove('open');
}
document.addEventListener('click', function(e) {
if (!document.getElementById('equipment-wrapper').contains(e.target)) {
csOpen = false;
document.getElementById('cs-trigger').classList.remove('open');
document.getElementById('cs-options').classList.remove('open');
}
});
async function loadModelInfo() {
try {
const res = await fetch('/model_info');
const data = await res.json();
if (data.error) { showToast('Model not ready: ' + data.error); return; }
document.getElementById('model-badge').textContent =
'LR=' + data.config.learning_rate + ' · N=' + data.config.n_estimators;
const rows = [
['Learning Rate', data.config.learning_rate],
['N Estimators', data.config.n_estimators],
['Split', data.config.train_ratio + '/' + data.config.val_ratio + '/' + data.config.test_ratio],
['Test AUC', (data.test_metrics.test_auc * 100).toFixed(2) + '%'],
['Test F1', (data.test_metrics.test_f1 * 100).toFixed(2) + '%'],
['Test Accuracy', (data.test_metrics.test_accuracy * 100).toFixed(2) + '%'],
['Precision', (data.test_metrics.test_precision* 100).toFixed(2) + '%'],
['Recall', (data.test_metrics.test_recall * 100).toFixed(2) + '%'],
];
document.getElementById('model-info').innerHTML = rows.map(([k,v],i) =>
'<div class="info-row"><span class="info-key">' + k + '</span>' +
'<span class="info-val' + (i >= 3 ? ' green' : '') + '">' + v + '</span></div>'
).join('');
} catch(e) {
document.getElementById('model-badge').textContent = 'Model Error';
}
}
async function runPredict() {
const btn = document.getElementById('predict-btn');
btn.classList.add('loading');
btn.disabled = true;
const payload = {
equipment: document.getElementById('equipment').value,
temperature: parseFloat(document.getElementById('temperature').value),
pressure: parseFloat(document.getElementById('pressure').value),
vibration: parseFloat(document.getElementById('vibration').value),
humidity: parseFloat(document.getElementById('humidity').value),
};
try {
const res = await fetch('/predict', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.error) { showToast('Error: ' + data.error); return; }
showResult(data, payload);
addHistory(data, payload);
} catch(e) {
showToast('Network error — please try again.');
} finally {
btn.classList.remove('loading');
btn.disabled = false;
}
}
function showResult(data, payload) {
const isFaulty = data.prediction === 1;
const prob = (data.probability * 100).toFixed(1);
const cls = isFaulty ? 'faulty' : 'healthy';
document.getElementById('result-card').className = 'result-card ' + cls;
document.getElementById('idle-state').style.display = 'none';
document.getElementById('result-content').style.display = 'block';
const vt = document.getElementById('verdict-text');
vt.className = 'verdict ' + cls;
vt.textContent = isFaulty ? '⚠ FAULT DETECTED' : '✓ HEALTHY';
document.getElementById('verdict-sub').textContent = isFaulty
? 'High fault probability — immediate inspection recommended.'
: 'Equipment readings within normal operating range.';
document.getElementById('prob-pct').textContent = prob + '%';
const fill = document.getElementById('prob-fill');
fill.className = 'prob-fill ' + cls;
setTimeout(() => fill.style.width = prob + '%', 50);
document.getElementById('mini-metrics').innerHTML = [
['Probability', prob + '%'],
['Confidence', data.confidence],
['Equipment', payload.equipment],
['Threshold', (data.threshold * 100).toFixed(0) + '%'],
].map(([k,v]) =>
'<div class="mini-metric"><div class="mm-val">' + v + '</div><div class="mm-key">' + k + '</div></div>'
).join('');
}
function addHistory(data, payload) {
const isFaulty = data.prediction === 1;
const cls = isFaulty ? 'faulty' : 'healthy';
const list = document.getElementById('history-list');
if (list.children.length === 1 && list.firstElementChild.style.color) list.innerHTML = '';
const item = document.createElement('div');
item.className = 'hist-item';
item.innerHTML =
'<div><div style="font-family:var(--mono);font-size:.78rem;">' + payload.equipment + '</div>' +
'<div class="hist-equip">T=' + payload.temperature + '° P=' + payload.pressure + 'bar V=' + payload.vibration + '</div></div>' +
'<span class="hist-badge ' + cls + '">' + (isFaulty ? 'FAULT' : 'OK') + ' · ' + (data.probability*100).toFixed(1) + '%</span>';
list.prepend(item);
if (list.children.length > 20) list.removeChild(list.lastChild);
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 3500);
}
loadModelInfo();
</script>
</body>
</html>"""
# ─────────────────────────────────────────────
# ROUTES
# ─────────────────────────────────────────────
@app.route("/")
def index():
options = "\n".join(
f'<option value="{e}">{e.capitalize()}</option>'
for e in EQUIPMENT_OPTIONS
)
return render_template_string(HTML, equipment_options=options)
@app.route("/model_info")
def model_info():
cfg = ARTIFACT["config"]
return jsonify({
"config": cfg,
"test_metrics": ARTIFACT["test_metrics"],
"cm": ARTIFACT["cm"],
})
@app.route("/predict", methods=["POST"])
def predict():
body = request.get_json(force=True)
try:
row = pd.DataFrame([{
"equipment": body["equipment"],
"temperature": float(body["temperature"]),
"pressure": float(body["pressure"]),
"vibration": float(body["vibration"]),
"humidity": float(body["humidity"]),
}])
except (KeyError, ValueError) as e:
return jsonify({"error": f"Bad input: {e}"}), 400
prob = float(ARTIFACT["pipeline"].predict_proba(row)[0, 1])
pred = int(prob >= THRESHOLD)
confidence = "HIGH" if prob > 0.85 or prob < 0.15 else "MEDIUM" if prob > 0.65 or prob < 0.35 else "LOW"
return jsonify({
"prediction": pred,
"probability": round(prob, 4),
"confidence": confidence,
"threshold": THRESHOLD,
"label": "FAULTY" if pred == 1 else "HEALTHY",
})
if __name__ == "__main__":
app.run(debug=False, host="0.0.0.0", port=7860)