techavenger123
Project Structure File.
2f37dc9
"""
FaultSense — LightGBM + Random Forest Fault Prediction App
Both models trained at startup; UI lets user switch between them.
"""
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
from sklearn.ensemble import RandomForestClassifier
import joblib
from lightgbm import LGBMClassifier
from flask import Flask, request, jsonify, render_template_string
# ─────────────────────────────────────────────
# CONFIG
# ─────────────────────────────────────────────
DATA_PATH = "synthetic_nim_parallel_10000.csv"
LGBM_PATH = "/tmp/faultsense_lgbm.joblib"
RF_PATH = "/tmp/faultsense_rf.joblib"
DROP_COLS = ["location"]
TARGET = "faulty"
CAT_COLS = ["equipment"]
NUM_COLS = ["temperature", "pressure", "vibration", "humidity"]
RANDOM_STATE = 42
THRESHOLD = 0.5
LGBM_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,
learning_rate=0.05,
n_estimators=165,
)
RF_PARAMS = dict(
n_estimators=165,
max_depth=10,
min_samples_split=10,
min_samples_leaf=5,
class_weight="balanced",
random_state=RANDOM_STATE,
n_jobs=-1,
)
BEST_CONFIG = {
"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 load_data(cfg):
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
)
return X_train, X_val, X_test, y_train, y_val, y_test
def compute_metrics(pipeline, X_test, y_test):
y_prob = pipeline.predict_proba(X_test)[:, 1]
y_pred = (y_prob >= THRESHOLD).astype(int)
return {
"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),
}, confusion_matrix(y_test, y_pred).tolist()
def train_lgbm(X_train, X_test, y_train, y_test):
print("Training LightGBM...")
pipeline = Pipeline([
("pre", make_preprocessor()),
("clf", LGBMClassifier(**LGBM_PARAMS))
])
pipeline.fit(X_train, y_train)
metrics, cm = compute_metrics(pipeline, X_test, y_test)
print(f"LGBM AUC={metrics['test_auc']} F1={metrics['test_f1']}")
return {"pipeline": pipeline, "test_metrics": metrics, "cm": cm,
"config": {**BEST_CONFIG, "model": "LightGBM",
"learning_rate": LGBM_PARAMS["learning_rate"],
"n_estimators": LGBM_PARAMS["n_estimators"]}}
def train_rf(X_train, X_test, y_train, y_test):
print("Training Random Forest...")
pipeline = Pipeline([
("pre", make_preprocessor()),
("clf", RandomForestClassifier(**RF_PARAMS))
])
pipeline.fit(X_train, y_train)
metrics, cm = compute_metrics(pipeline, X_test, y_test)
print(f"RF AUC={metrics['test_auc']} F1={metrics['test_f1']}")
return {"pipeline": pipeline, "test_metrics": metrics, "cm": cm,
"config": {**BEST_CONFIG, "model": "Random Forest",
"n_estimators": RF_PARAMS["n_estimators"],
"max_depth": RF_PARAMS["max_depth"]}}
def load_or_train_all():
X_train, X_val, X_test, y_train, y_val, y_test = load_data(BEST_CONFIG)
if os.path.exists(LGBM_PATH):
print(f"Loading LGBM from {LGBM_PATH}")
lgbm_artifact = joblib.load(LGBM_PATH)
else:
lgbm_artifact = train_lgbm(X_train, X_test, y_train, y_test)
joblib.dump(lgbm_artifact, LGBM_PATH)
if os.path.exists(RF_PATH):
print(f"Loading RF from {RF_PATH}")
rf_artifact = joblib.load(RF_PATH)
else:
rf_artifact = train_rf(X_train, X_test, y_train, y_test)
joblib.dump(rf_artifact, RF_PATH)
return {"lgbm": lgbm_artifact, "rf": rf_artifact}
# ─────────────────────────────────────────────
# LOAD MODELS AT MODULE LEVEL
# ─────────────────────────────────────────────
ARTIFACTS = load_or_train_all()
# ─────────────────────────────────────────────
# 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; --accent4: #f59e0b;
--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: 1200px; margin: 0 auto; padding: 40px 24px 80px; }
header { display: flex; align-items: center; gap: 16px; margin-bottom: 32px; 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; }
/* MODEL SELECTOR TABS */
.model-tabs { display: flex; gap: 10px; margin-bottom: 24px; }
.model-tab {
flex: 1; padding: 14px 20px; border-radius: 12px; border: 1px solid var(--border);
background: var(--surface); cursor: pointer; font-family: var(--mono);
font-size: .8rem; color: var(--muted); transition: all .2s; text-align: center;
display: flex; flex-direction: column; gap: 4px; align-items: center;
}
.model-tab:hover { border-color: var(--accent); color: var(--text); }
.model-tab.active.lgbm { border-color: var(--accent); background: rgba(0,229,160,.08); color: var(--accent); }
.model-tab.active.rf { border-color: var(--accent4); background: rgba(245,158,11,.08); color: var(--accent4); }
.model-tab .tab-name { font-size: .95rem; font-weight: 700; }
.model-tab .tab-desc { font-size: .65rem; color: inherit; opacity: .7; }
.tab-auc { font-size: .7rem; opacity: .85; margin-top: 2px; }
.main-grid { display: grid; grid-template-columns: 1fr 400px; gap: 24px; align-items: start; }
@media (max-width: 900px) { .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.rf-active { background: var(--accent4); }
.btn-predict.rf-active:hover { box-shadow: 0 0 32px rgba(245,158,11,.5); }
.btn-predict:disabled { background: var(--muted); cursor: not-allowed; transform: none; box-shadow: 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: 8px; }
.model-used-tag { display: inline-block; font-family: var(--mono); font-size: .65rem; padding: 3px 8px; border-radius: 6px; margin-bottom: 20px; }
.model-used-tag.lgbm { background: rgba(0,229,160,.12); color: var(--accent); border: 1px solid rgba(0,229,160,.3); }
.model-used-tag.rf { background: rgba(245,158,11,.12); color: var(--accent4); border: 1px solid rgba(245,158,11,.3); }
.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); }
/* METRICS COMPARISON TABLE */
.compare-table { width: 100%; border-collapse: collapse; }
.compare-table th { font-family: var(--mono); font-size: .65rem; letter-spacing: 1px; text-transform: uppercase; color: var(--muted); padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--border); }
.compare-table th.lgbm-col { color: var(--accent); }
.compare-table th.rf-col { color: var(--accent4); }
.compare-table td { font-family: var(--mono); font-size: .78rem; padding: 9px 10px; border-bottom: 1px solid var(--border); }
.compare-table tr:last-child td { border-bottom: none; }
.compare-table td.metric-name { color: var(--muted); font-size: .7rem; }
.compare-table td.win { font-weight: 700; }
.compare-table td.win.lgbm { color: var(--accent); }
.compare-table td.win.rf { color: var(--accent4); }
.win-tag { font-size: .55rem; padding: 1px 5px; border-radius: 4px; margin-left: 5px; vertical-align: middle; }
.win-tag.lgbm { background: rgba(0,229,160,.15); color: var(--accent); }
.win-tag.rf { background: rgba(245,158,11,.15); color: var(--accent4); }
.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); }
.info-val.amber { color: var(--accent4); }
.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); }
.hist-model-tag { font-family: var(--mono); font-size: .58rem; padding: 2px 6px; border-radius: 4px; margin-top: 3px; display: inline-block; }
.hist-model-tag.lgbm { background: rgba(0,229,160,.1); color: var(--accent); }
.hist-model-tag.rf { background: rgba(245,158,11,.1); color: var(--accent4); }
.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>Multi-Model Equipment Fault Predictor</p>
</div>
<div class="badge" id="model-badge">Loading Models…</div>
</header>
<!-- MODEL SELECTOR TABS -->
<div class="model-tabs" id="model-tabs">
<div class="model-tab active lgbm" id="tab-lgbm" onclick="selectModel('lgbm')">
<span class="tab-name">⚡ LightGBM</span>
<span class="tab-desc">Gradient Boosting</span>
<span class="tab-auc" id="lgbm-tab-auc">Loading…</span>
</div>
<div class="model-tab rf" id="tab-rf" onclick="selectModel('rf')">
<span class="tab-name">🌲 Random Forest</span>
<span class="tab-desc">Ensemble Trees</span>
<span class="tab-auc" id="rf-tab-auc">Loading…</span>
</div>
</div>
<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 lgbm-active" id="predict-btn" onclick="runPredict()">
<span class="btn-text">⚡ Run Prediction</span>
<div class="spinner"></div>
</button>
</div>
<!-- METRICS COMPARISON -->
<div class="card">
<div class="card-title">Model Comparison</div>
<div id="compare-content">
<div style="color:var(--muted);font-size:.8rem;font-family:var(--mono);text-align:center;padding:12px 0;">Loading…</div>
</div>
</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>Select a model, enter sensor<br>readings, 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 id="model-used-tag" class="model-used-tag lgbm"></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" id="model-config-title">Active Model Config</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;
let activeModel = 'lgbm';
let modelData = {};
function selectModel(model) {
activeModel = model;
document.getElementById('tab-lgbm').className = 'model-tab' + (model === 'lgbm' ? ' active lgbm' : '');
document.getElementById('tab-rf').className = 'model-tab' + (model === 'rf' ? ' active rf' : '');
const btn = document.getElementById('predict-btn');
btn.className = model === 'lgbm' ? 'btn-predict lgbm-active' : 'btn-predict rf-active';
btn.querySelector('.btn-text').textContent = model === 'lgbm' ? '⚡ Run Prediction' : '🌲 Run Prediction';
updateModelInfo(model);
}
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');
modelData = await res.json();
if (modelData.error) { showToast('Model error: ' + modelData.error); return; }
const lg = modelData.lgbm, rf = modelData.rf;
document.getElementById('lgbm-tab-auc').textContent = 'AUC ' + (lg.test_metrics.test_auc * 100).toFixed(1) + '%';
document.getElementById('rf-tab-auc').textContent = 'AUC ' + (rf.test_metrics.test_auc * 100).toFixed(1) + '%';
document.getElementById('model-badge').textContent = '2 Models Ready';
// Build comparison table
const metrics = [
['AUC', 'test_auc'],
['Accuracy', 'test_accuracy'],
['Precision', 'test_precision'],
['Recall', 'test_recall'],
['F1 Score', 'test_f1'],
['Log Loss', 'test_logloss'],
];
const rows = metrics.map(([label, key]) => {
const lv = lg.test_metrics[key], rv = rf.test_metrics[key];
const higherBetter = key !== 'test_logloss';
const lgWins = higherBetter ? lv > rv : lv < rv;
const rfWins = higherBetter ? rv > lv : rv < lv;
const fmt = v => key === 'test_logloss' ? v.toFixed(4) : (v * 100).toFixed(2) + '%';
return `<tr>
<td class="metric-name">${label}</td>
<td class="${lgWins ? 'win lgbm' : ''}">${fmt(lv)}${lgWins ? '<span class="win-tag lgbm">▲</span>' : ''}</td>
<td class="${rfWins ? 'win rf' : ''}">${fmt(rv)}${rfWins ? '<span class="win-tag rf">▲</span>' : ''}</td>
</tr>`;
}).join('');
document.getElementById('compare-content').innerHTML = `
<table class="compare-table">
<thead><tr>
<th>Metric</th>
<th class="lgbm-col">⚡ LightGBM</th>
<th class="rf-col">🌲 Random Forest</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>`;
updateModelInfo('lgbm');
} catch(e) {
document.getElementById('model-badge').textContent = 'Load Error';
}
}
function updateModelInfo(model) {
if (!modelData[model]) return;
const d = modelData[model];
const isLgbm = model === 'lgbm';
const color = isLgbm ? 'green' : 'amber';
const rows = isLgbm ? [
['Model', 'LightGBM'],
['Learning Rate', d.config.learning_rate],
['N Estimators', d.config.n_estimators],
['Split', d.config.train_ratio + '/' + d.config.val_ratio + '/' + d.config.test_ratio],
] : [
['Model', 'Random Forest'],
['N Estimators', d.config.n_estimators],
['Max Depth', d.config.max_depth],
['Split', d.config.train_ratio + '/' + d.config.val_ratio + '/' + d.config.test_ratio],
];
const metricRows = [
['Test AUC', (d.test_metrics.test_auc * 100).toFixed(2) + '%'],
['Test F1', (d.test_metrics.test_f1 * 100).toFixed(2) + '%'],
['Test Accuracy', (d.test_metrics.test_accuracy * 100).toFixed(2) + '%'],
['Precision', (d.test_metrics.test_precision * 100).toFixed(2) + '%'],
['Recall', (d.test_metrics.test_recall * 100).toFixed(2) + '%'],
];
document.getElementById('model-info').innerHTML =
[...rows, ...metricRows].map(([k, v], i) =>
'<div class="info-row"><span class="info-key">' + k + '</span>' +
'<span class="info-val' + (i >= rows.length ? ' ' + color : '') + '">' + v + '</span></div>'
).join('');
}
async function runPredict() {
const btn = document.getElementById('predict-btn');
btn.classList.add('loading');
btn.disabled = true;
const payload = {
model: activeModel,
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';
const isLgbm = data.model === 'lgbm';
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.';
const tag = document.getElementById('model-used-tag');
tag.className = 'model-used-tag ' + (isLgbm ? 'lgbm' : 'rf');
tag.textContent = isLgbm ? '⚡ LightGBM' : '🌲 Random Forest';
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 isLgbm = data.model === 'lgbm';
const list = document.getElementById('history-list');
if (list.children.length === 1 && list.firstElementChild.style.color !== undefined
&& list.firstElementChild.querySelector) list.innerHTML = '';
if (list.children.length === 1 && !list.firstElementChild.classList.contains('hist-item')) 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>' +
'<span class="hist-model-tag ' + (isLgbm ? 'lgbm' : 'rf') + '">' + (isLgbm ? '⚡ LGBM' : '🌲 RF') + '</span></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():
return render_template_string(HTML)
@app.route("/model_info")
def model_info():
return jsonify({
"lgbm": {
"config": ARTIFACTS["lgbm"]["config"],
"test_metrics": ARTIFACTS["lgbm"]["test_metrics"],
"cm": ARTIFACTS["lgbm"]["cm"],
},
"rf": {
"config": ARTIFACTS["rf"]["config"],
"test_metrics": ARTIFACTS["rf"]["test_metrics"],
"cm": ARTIFACTS["rf"]["cm"],
},
})
@app.route("/predict", methods=["POST"])
def predict():
body = request.get_json(force=True)
model_key = body.get("model", "lgbm")
if model_key not in ARTIFACTS:
return jsonify({"error": f"Unknown model '{model_key}'. Use 'lgbm' or 'rf'."}), 400
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
artifact = ARTIFACTS[model_key]
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({
"model": model_key,
"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)