Pro-Coder's picture
Upload 34 files
f9c247c verified
Raw
History Blame Contribute Delete
32.1 kB
"""
Smart Warehouse AI Assistant
=============================
A Daifuku-style intralogistics AI copilot demo, built for a Hugging Face
Space. Combines:
1. An LLM-powered assistant (RAG: TF-IDF retrieval + hosted LLM via the
HF Inference API) for natural-language warehouse operations Q&A.
2. A TF-IDF + Logistic Regression intent classifier that routes queries
into 8 operational categories.
3. A lightweight NL -> structured query layer over synthetic inventory /
order tables.
4. An Isolation Forest anomaly detector for conveyor/crane sensor
streams (predictive maintenance).
5. A Model Evaluation tab reporting real accuracy/F1/ROC-AUC metrics
computed by build_artifacts.py.
Author: (your name here) -- built as an application project for Daifuku Co., Ltd.
"""
import json
import os
import gradio as gr
import pandas as pd
from src.anomaly_model import FEATURES as SENSOR_FEATURES
from src.anomaly_model import load_artifacts as load_anomaly_artifacts
from src.anomaly_model import score_reading
from src.data_generation import generate_inventory_db, generate_orders_db
from src.intent_model import INTENT_DESCRIPTIONS, load_pipeline, predict as intent_predict
from src.inventory_db import query_inventory, query_orders
from src.llm_client import answer_query, test_connection, _get_hf_token, TOKEN_ENV_VAR_CANDIDATES
from src.retriever import KBRetriever
ROOT = os.path.dirname(os.path.abspath(__file__))
MODELS_DIR = os.path.join(ROOT, "models")
DATA_DIR = os.path.join(ROOT, "data")
ASSETS_DIR = os.path.join(ROOT, "assets")
# --------------------------------------------------------------------------
# Load pre-trained artifacts (fast: no training happens at Space startup
# under normal conditions). If unpickling fails -- e.g. the models were
# built with a different scikit-learn version than the one installed in
# this container -- we transparently rebuild everything from scratch. This
# is fast (a few seconds, see build_artifacts.py) and makes the app immune
# to sklearn version-pinning mismatches between build time and deploy time.
# --------------------------------------------------------------------------
def _load_or_rebuild_artifacts():
intent_path = os.path.join(MODELS_DIR, "intent_pipeline.joblib")
anomaly_model_path = os.path.join(MODELS_DIR, "anomaly_iforest.joblib")
anomaly_scaler_path = os.path.join(MODELS_DIR, "anomaly_scaler.joblib")
def _try_load():
pipeline = load_pipeline(intent_path)
model, scaler = load_anomaly_artifacts(anomaly_model_path, anomaly_scaler_path)
# Smoke-test the loaded pipeline against the exact code path the app
# uses at request time. On a scikit-learn version mismatch, sklearn
# sometimes unpickles "successfully" but throws later on first real
# use (e.g. LogisticRegression missing an internal attribute) --
# catching that here, at import time, is what makes this self-healing.
intent_predict(pipeline, "healthcheck")
score_reading(model, scaler, {"motor_temp_c": 55, "vibration_mm_s": 2.2, "current_amps": 12, "belt_speed_mps": 1.5})
return pipeline, model, scaler
try:
return _try_load()
except Exception as e: # noqa: BLE001 -- any load/version issue triggers a rebuild
print(f"[startup] Could not load pre-built model artifacts ({type(e).__name__}: {e}). "
f"Rebuilding from scratch with the installed scikit-learn version...")
import build_artifacts
build_artifacts.build_intent_classifier()
build_artifacts.build_anomaly_detector()
build_artifacts.build_retrieval_eval()
build_artifacts.build_inventory_and_orders()
print("[startup] Rebuild complete.")
return _try_load()
intent_pipeline, anomaly_model, anomaly_scaler = _load_or_rebuild_artifacts()
retriever = KBRetriever()
# Prefer the pre-generated CSVs (so demo state matches the eval run); fall back to
# regenerating in-memory if they're missing for some reason.
try:
inventory_df = pd.read_csv(os.path.join(DATA_DIR, "inventory.csv"))
orders_df = pd.read_csv(os.path.join(DATA_DIR, "orders.csv"))
except FileNotFoundError:
inventory_df = generate_inventory_db()
orders_df = generate_orders_db()
def _load_json(name):
path = os.path.join(DATA_DIR, name)
if os.path.exists(path):
with open(path) as f:
return json.load(f)
return {}
intent_eval = _load_json("intent_eval.json")
anomaly_eval = _load_json("anomaly_eval.json")
retrieval_eval = _load_json("retrieval_eval.json")
latency_eval = _load_json("latency_eval.json")
HF_TOKEN_SET = bool(_get_hf_token())
# --------------------------------------------------------------------------
# ZeroGPU compatibility shim
# --------------------------------------------------------------------------
# This app is CPU-only by design (scikit-learn locally, LLM calls go to the
# remote HF Inference API). Some Spaces accounts, however, only offer the
# free "ZeroGPU" hardware tier, which requires at least one function
# decorated with `@spaces.GPU` to be present or the platform's startup
# check fails with "No @spaces.GPU function detected". This is a harmless,
# unused health-check function that satisfies that requirement without
# changing any real behaviour -- it is never called on the request path.
try:
import spaces
@spaces.GPU(duration=5)
def _zerogpu_healthcheck():
return True
except ImportError:
# Running locally / on CPU-basic hardware where `spaces` isn't installed.
def _zerogpu_healthcheck():
return True
# ==========================================================================
# TAB 1 -- AI Assistant (LLM + RAG + intent routing)
# ==========================================================================
def chat_fn(message, history):
intent = intent_predict(intent_pipeline, message)
response = answer_query(message, retriever)
intent_label = INTENT_DESCRIPTIONS.get(intent.intent, intent.intent)
meta_lines = [f"**Detected intent:** {intent_label} ({intent.confidence:.0%} confidence)"]
if response.sources:
src_str = ", ".join(f"{s.title} ({s.score:.2f})" for s in response.sources)
meta_lines.append(f"**Retrieved context:** {src_str}")
meta_lines.append(
f"**Generation:** {'LLM (' + response.model_id + ')' if response.used_llm else 'retrieval-only fallback'}"
f" · {response.latency_s * 1000:.0f} ms"
)
if not response.used_llm and response.debug_errors:
err_lines = "\n".join(f" - `{e}`" for e in response.debug_errors)
meta_lines.append(f"**Why the LLM wasn't used:**\n{err_lines}")
full_reply = response.answer + "\n\n---\n" + "\n".join(meta_lines)
return full_reply
def test_llm_fn():
response = test_connection(retriever)
if response.used_llm:
return (
f"✅ **LLM connection working.** Model: `{response.model_id}` · "
f"{response.latency_s * 1000:.0f} ms\n\nSample answer: {response.answer}"
)
err_lines = "\n".join(f"- `{e}`" for e in response.debug_errors) or "(no error detail captured)"
return (
"❌ **LLM connection failed** — running in retrieval-only fallback mode.\n\n"
f"**Errors from each candidate model tried:**\n{err_lines}\n\n"
"**Common causes:** missing/invalid `HF_TOKEN` secret, the token's account "
"lacking Inference API access, or the candidate models being temporarily "
"unavailable on HF's free serverless tier. See `src/llm_client.py` to add "
"or reorder candidate models, or set the `LLM_MODEL_ID` secret to force a "
"specific one."
)
ASSISTANT_EXAMPLES = [
"The conveyor belt in Zone C is making noise",
"How many units of SKU-1042 are in Zone B?",
"What's the difference between an AGV and an AMR?",
"A forklift near-miss was reported in Zone A",
"What's the fastest picking route for a high volume order?",
"Is Crane-03 operational?",
]
# ==========================================================================
# TAB 2 -- Inventory & Order Query
# ==========================================================================
def inventory_query_fn(text):
intent = intent_predict(intent_pipeline, text)
if intent.intent == "order_status":
result = query_orders(orders_df, text)
note = "Interpreted as an **order status** query."
else:
result = query_inventory(inventory_df, text)
note = "Interpreted as an **inventory** query."
if result.empty:
result = pd.DataFrame({"message": ["No matching records found for this query."]})
return note, result
# ==========================================================================
# TAB 3 -- Predictive Maintenance / Anomaly Detection
# ==========================================================================
def anomaly_fn(motor_temp, vibration, current, belt_speed):
reading = {
"motor_temp_c": motor_temp,
"vibration_mm_s": vibration,
"current_amps": current,
"belt_speed_mps": belt_speed,
}
result = score_reading(anomaly_model, anomaly_scaler, reading)
verdict = "🔴 ANOMALY DETECTED" if result.is_anomaly else "🟢 Normal operating range"
detail = (
f"### {verdict}\n\n"
f"**Anomaly score:** {result.anomaly_score:.2f} / 1.00\n\n"
f"| Sensor | Value | Typical normal range |\n"
f"|---|---|---|\n"
f"| Motor temperature | {motor_temp:.1f} °C | 47–63 °C |\n"
f"| Vibration | {vibration:.2f} mm/s | 1.2–3.2 mm/s |\n"
f"| Motor current | {current:.1f} A | 9–15 A |\n"
f"| Belt speed | {belt_speed:.2f} m/s | 1.2–1.8 m/s |\n"
)
if result.is_anomaly:
detail += (
"\n**Recommended action:** Flag for inspection. Elevated temperature + "
"vibration + current with reduced belt speed typically indicates bearing "
"wear, belt misalignment, or motor overload -- schedule maintenance before "
"the next shift to avoid an unplanned stoppage."
)
return detail
ANOMALY_PRESETS = {
"Normal reading": (55.0, 2.2, 12.0, 1.5),
"Early bearing wear": (68.0, 3.8, 15.5, 1.3),
"Severe fault (imminent failure)": (85.0, 6.5, 21.0, 0.6),
}
def load_preset(name):
return ANOMALY_PRESETS[name]
# ==========================================================================
# TAB 4 -- Model Evaluation
# ==========================================================================
def eval_intent_section_md():
cls_report = intent_eval.get("classification_report", {})
per_class_rows = []
for cls in intent_eval.get("classes", []):
stats = cls_report.get(cls, {})
per_class_rows.append(
f"| {cls} | {stats.get('precision', 0):.2f} | {stats.get('recall', 0):.2f} | "
f"{stats.get('f1-score', 0):.2f} | {int(stats.get('support', 0))} |"
)
per_class_table = "\n".join(per_class_rows)
return f"""
## 1. Intent Classifier (TF-IDF + Logistic Regression)
**What it does:** routes a free-text query (e.g. *"The conveyor belt in Zone C
is making noise"*) into one of 8 operational categories, used by both the
AI Assistant and Inventory & Order Query tabs to decide how to handle a
request.
**Training data:** {intent_eval.get('n_train', '?') } synthetically generated
example queries (see chart below), built from ~8 hand-written templates per
category with randomised SKU codes, zone names, order IDs, and equipment IDs
slotted in -- e.g. *"How many units of {{sku}} are in {{zone}}?"*. This keeps
the language varied while being fully reproducible (`src/data_generation.py`).
Evaluated on a **held-out stratified test split** of {intent_eval.get('n_test', '?')}
examples the model never saw during training.
| Metric | Score |
|---|---|
| **Accuracy** | **{intent_eval.get('accuracy', 0):.2%}** |
| **Macro F1** | **{intent_eval.get('macro_f1', 0):.2%}** |
**Per-class performance:**
| Intent | Precision | Recall | F1 | Support |
|---|---|---|---|---|
{per_class_table}
"""
def eval_anomaly_section_md():
return f"""
## 2. Predictive Maintenance Anomaly Detector (Isolation Forest)
**What it does:** flags abnormal conveyor/crane motor sensor readings
(temperature, vibration, current, belt speed) before they cause an
unplanned stoppage -- powers the Predictive Maintenance tab.
**Training data:** {900 + 100} synthetic sensor readings (900 "normal"
+ 100 "anomaly" patterns), each with 4 features. Normal readings are drawn
from realistic operating ranges (e.g. ~55°C motor temp, ~2.2 mm/s vibration);
anomalies simulate bearing wear / misalignment / overload (elevated temp,
vibration, and current with reduced belt speed). See the distribution chart
below for exactly how these two classes differ.
The model itself is trained **unsupervised** (Isolation Forest never sees
the anomaly label during fitting) -- labels are used only to *evaluate* it
afterward, on a held-out test split of {anomaly_eval.get('n_test', '?')}
readings ({anomaly_eval.get('test_anomaly_rate', 0):.1%} true anomaly rate).
| Metric | Score |
|---|---|
| **Precision** | **{anomaly_eval.get('precision', 0):.2%}** |
| **Recall** | **{anomaly_eval.get('recall', 0):.2%}** |
| **F1 Score** | **{anomaly_eval.get('f1', 0):.2%}** |
| **ROC-AUC** | **{anomaly_eval.get('roc_auc', 0):.3f}** |
| Accuracy | {anomaly_eval.get('accuracy', 0):.2%} |
"""
def eval_retrieval_section_md():
retrieval_rows = "\n".join(
f"| {r['query']} | {r['expected']} | {r['retrieved_top1']} | "
f"{'✅' if r['hit@1'] else ('〰️' if r['hit@2'] else '❌')} | {r['top1_score']:.2f} |"
for r in retrieval_eval.get("rows", [])
)
return f"""
## 3. Retrieval (RAG) Evaluation
**What it does:** before the LLM answers a question, this component finds
the most relevant passages from a 10-article warehouse-operations knowledge
base (AS/RS, AGV/AMR, WMS, sortation, picking strategy, safety, etc. -- see
`src/knowledge_base.py`) using TF-IDF + cosine similarity, so the LLM answers
from real context rather than guessing.
**Evaluation data:** {retrieval_eval.get('n_queries', '?')} hand-labelled
(query, expected-article) pairs -- a small ground-truth set built by hand to
check the retriever finds the *right* article, not just *an* article.
| Metric | Score |
|---|---|
| **Hit Rate @ 1** | **{retrieval_eval.get('hit_rate_at_1', 0):.0%}** |
| **Hit Rate @ 2** | **{retrieval_eval.get('hit_rate_at_2', 0):.0%}** |
| Query | Expected Doc | Retrieved (top-1) | Hit | Score |
|---|---|---|---|---|
{retrieval_rows}
"""
def eval_latency_section_md():
return f"""
## 4. Latency Benchmark (per-request, CPU)
Average of 50 runs each, measured on the same CPU hardware the Space runs on.
| Component | Avg. latency |
|---|---|
| Intent classification | {latency_eval.get('intent_classifier_ms', '?')} ms |
| Anomaly scoring | {latency_eval.get('anomaly_detector_ms', '?')} ms |
| KB retrieval (TF-IDF) | {latency_eval.get('kb_retrieval_ms', '?')} ms |
| LLM generation | Depends on the hosted Inference API (measured live per-request in the Assistant tab, not benchmarked here) |
"""
EVAL_METHODOLOGY_MD = """
### Evaluation methodology notes
- All datasets are **synthetically generated** (see `src/data_generation.py`) using
templated-but-varied natural language and randomised sensor distributions with a
fixed seed, so results are fully reproducible via `python build_artifacts.py`.
- The intent classifier and anomaly detector are evaluated on a **held-out test
split** they never saw during training (stratified, 25% / 30% respectively).
- The anomaly detector itself is trained **unsupervised** (Isolation Forest never
sees the `label` column during `.fit()`); labels are used only to *evaluate* it,
mirroring how you'd validate an anomaly model against a small set of confirmed
historical incidents in production.
- In a production deployment, all three components would be continuously
re-evaluated against real WMS/WCS/sensor logs rather than synthetic data.
"""
# ==========================================================================
# TAB 5 -- About
# ==========================================================================
ABOUT_MD = f"""
# 🏭 Smart Warehouse AI Assistant
**A portfolio project demonstrating an applied-AI approach to intralogistics
operations, built as part of a job application to Daifuku Co., Ltd.**
## What this demonstrates
Daifuku builds material handling and automation systems -- AS/RS, conveyors
and sortation, AGVs/AMRs, and the software (WMS/WCS) that orchestrates them.
This project is a compact but end-to-end example of how an AI layer can sit
on top of that kind of system:
| Capability | Where |
|---|---|
| **LLM-powered natural-language assistant**, grounded with retrieval (RAG) so it answers from real warehouse-ops knowledge rather than hallucinating | *AI Assistant* tab |
| **Intent classification** to route free-text requests (maintenance, safety, navigation, inventory, etc.) the way a real ops system would triage tickets | *AI Assistant* / *Inventory* tabs |
| **Predictive maintenance** via unsupervised anomaly detection on conveyor/crane sensor streams -- catching bearing wear or misalignment before an unplanned stoppage | *Predictive Maintenance* tab |
| **NL-to-structured-query** over inventory/order data, a lightweight stand-in for a WMS query tool | *Inventory & Order Query* tab |
| **Rigorous, reproducible evaluation** of every ML component (accuracy, F1, ROC-AUC, retrieval hit-rate, latency) rather than just a demo that "looks like it works" | *Model Evaluation* tab |
## Tech stack
- **UI / deployment:** [Gradio](https://gradio.app) on Hugging Face Spaces
- **LLM:** Hosted instruct model via the Hugging Face **Inference API**
(`huggingface_hub.InferenceClient`), configurable via the `LLM_MODEL_ID`
env var. Falls back gracefully to a retrieval-only answer if no `HF_TOKEN`
is configured, so the public demo never breaks.
- **Retrieval:** TF-IDF + cosine similarity over a small hand-written
warehouse-operations knowledge base (simple, fast, fully local RAG).
- **Intent classification:** TF-IDF + Logistic Regression (scikit-learn) --
chosen deliberately over a heavier transformer classifier because it
trains in under a second and comfortably reaches **{intent_eval.get('accuracy', 0):.0%}
accuracy** on this task; right-sizing the model to the problem.
- **Anomaly detection:** Isolation Forest (scikit-learn), trained
unsupervised on scaled sensor features.
- **Evaluation:** scikit-learn metrics + matplotlib, all computed by
`build_artifacts.py` and saved as static artifacts the app loads at
startup (fast, reproducible Space boot).
## Architecture
```
┌───────────────────────┐
User query ───► │ Intent Classifier │ (TF-IDF + LogisticRegression)
└──────────┬────────────┘
│ intent label
┌───────────────────────┐
│ KB Retriever (RAG) │ (TF-IDF cosine similarity)
└──────────┬────────────┘
│ top-k passages
┌───────────────────────┐
│ Hosted LLM │ (HF Inference API)
│ (or extractive │
│ fallback if offline) │
└──────────┬────────────┘
Grounded answer
Sensor stream ───► StandardScaler ───► IsolationForest ───► anomaly / normal
```
## Why this matters for Daifuku
Modern intralogistics platforms generate huge volumes of operational data --
equipment telemetry, WMS transactions, safety logs. The value of AI here isn't
a flashy chatbot; it's **routing, grounding, and reliability**: correctly
triaging a request, answering from real operational context instead of
guessing, and flagging equipment problems before they cause downtime. This
project tries to demonstrate that mindset in miniature, with honest,
reproducible evaluation numbers rather than cherry-picked demo runs.
## Limitations & next steps
- All data here is **synthetic**, for portfolio/demo purposes -- a production
version would connect to real WMS/WCS APIs and historical sensor logs.
- The intent set (8 classes) and knowledge base (10 articles) are intentionally
small to keep the demo fast and auditable; both are easy to extend.
- The anomaly detector uses 4 hand-picked features; a production system would
likely use a richer multivariate sensor set and a supervised or
semi-supervised model once labelled failure data is available.
---
*Built as an application project. Source code available on request / in the
linked repository. Feedback welcome.*
"""
# ==========================================================================
# GRADIO APP
# ==========================================================================
CUSTOM_CSS = """
#title-banner { text-align: center; margin-bottom: 0.5em; }
.gradio-container { max-width: 1150px !important; margin: auto; }
"""
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=CUSTOM_CSS, title="Smart Warehouse AI Assistant") as demo:
gr.Markdown(
"<h1 id='title-banner'>🏭 Smart Warehouse AI Assistant</h1>"
"<p style='text-align:center; color:gray;'>LLM-powered intralogistics copilot · "
"intent routing · predictive maintenance · retrieval-grounded Q&A</p>"
)
if not HF_TOKEN_SET:
gr.Markdown(
"> ⚠️ **No HF token secret detected.** The AI Assistant tab will run in "
"**retrieval-only fallback mode** (still functional, just not LLM-generated "
f"prose). Add a secret named `HF_TOKEN` (or one of: {', '.join(TOKEN_ENV_VAR_CANDIDATES[1:])}) "
"in *Space settings → Variables and secrets* to enable full LLM responses."
)
with gr.Tab("💬 AI Assistant"):
gr.Markdown(
"Ask about equipment status, maintenance, safety, inventory, order status, "
"AGV routing, picking strategy, or general warehouse-automation concepts. "
"Answers are grounded (RAG) in a small warehouse-operations knowledge base."
)
with gr.Accordion("ℹ️ About the data behind this tab", open=False):
gr.Markdown(
"- **Knowledge base (RAG source):** 10 original, hand-written articles "
"covering AS/RS, AGV/AMR, WMS, conveyor/sortation, picking strategy, "
"predictive maintenance, safety protocol, inventory accuracy, KPIs, and "
"energy efficiency (`src/knowledge_base.py`). Every answer's *Retrieved "
"context* line shows exactly which article(s) it drew on.\n"
"- **Intent classifier:** trained on ~480 synthetically generated example "
"queries across 8 categories (see the Model Evaluation tab for accuracy).\n"
"- **LLM:** a hosted instruct model called via the Hugging Face Inference "
"API — not run locally. If no `HF_TOKEN` is configured, or the API call "
"fails, this tab automatically falls back to showing the retrieved "
"knowledge-base passages directly, so it never just breaks."
)
gr.ChatInterface(
fn=chat_fn,
type="messages",
examples=ASSISTANT_EXAMPLES,
chatbot=gr.Chatbot(height=430, label="Warehouse Assistant", type="messages"),
textbox=gr.Textbox(placeholder="e.g. The conveyor belt in Zone C is making noise"),
)
with gr.Accordion("🔧 LLM connection diagnostics", open=False):
gr.Markdown(
"If the assistant keeps answering in retrieval-only fallback mode, "
"click below to test the LLM connection directly and see the exact "
"error from each candidate model."
)
test_llm_btn = gr.Button("Test LLM connection")
test_llm_output = gr.Markdown()
test_llm_btn.click(test_llm_fn, inputs=None, outputs=test_llm_output)
with gr.Tab("📦 Inventory & Order Query"):
gr.Markdown(
"Type a natural-language inventory or order question. The intent classifier "
"decides whether to query the inventory table or the orders table, then "
"extracts SKU / order-id / zone slots to filter the result."
)
with gr.Accordion("ℹ️ About the data behind this tab", open=False):
gr.Markdown(
f"- **Inventory table:** {len(inventory_df)} synthetic SKUs across 5 "
"categories (Electronics, Apparel, Automotive Parts, Food & Beverage, "
"Household) and 4 warehouse zones, with randomised on-hand quantities, "
"reorder points, and unit costs.\n"
f"- **Orders table:** {len(orders_df)} synthetic orders with randomised "
"status (Received / Picking / Packed / Shipped / Delayed), line count, "
"priority, and zone.\n"
"- Both tables are generated by `src/data_generation.py` with a fixed "
"random seed, so they're reproducible but **not real operational data** "
"-- this is a stand-in for a live WMS/WCS query interface.\n"
"- Query parsing is regex-based slot extraction (SKU codes like `SKU-1042`, "
"order IDs like `#10007`, zone names) combined with the same intent "
"classifier used in the AI Assistant tab (`src/inventory_db.py`)."
)
with gr.Row():
inv_input = gr.Textbox(label="Query", placeholder="How many units of SKU-1042 are in Zone B?", scale=4)
inv_btn = gr.Button("Search", variant="primary", scale=1)
inv_note = gr.Markdown()
inv_result = gr.Dataframe(label="Results", wrap=True)
inv_btn.click(inventory_query_fn, inputs=inv_input, outputs=[inv_note, inv_result])
inv_input.submit(inventory_query_fn, inputs=inv_input, outputs=[inv_note, inv_result])
gr.Examples(
examples=[
"How many units of SKU-1042 are in Zone B?",
"What's the status of order #10007?",
"Show me low stock items",
"Any delayed orders?",
],
inputs=inv_input,
)
with gr.Accordion("Browse full tables", open=False):
gr.Markdown("**Inventory** (synthetic)")
gr.Dataframe(inventory_df, wrap=True)
gr.Markdown("**Orders** (synthetic)")
gr.Dataframe(orders_df, wrap=True)
with gr.Tab("⚠️ Predictive Maintenance"):
gr.Markdown(
"Enter live (or hypothetical) conveyor/crane motor sensor readings to check "
"for anomalous behaviour using an Isolation Forest model trained on "
"historical sensor patterns."
)
with gr.Accordion("ℹ️ About the data behind this tab", open=False):
gr.Markdown(
"- **Training data:** 1,000 synthetic sensor readings (900 normal + 100 "
"anomalous) across 4 features -- motor temperature, vibration, motor "
"current, and belt speed -- generated by `src/data_generation.py`. "
"Anomalies simulate realistic failure signatures: elevated temperature, "
"vibration, and current combined with reduced/erratic belt speed (the "
"pattern of bearing wear, belt misalignment, or motor overload).\n"
"- **Model:** Isolation Forest (unsupervised) trained on scaled features "
"-- it never sees a 'this is an anomaly' label during training, only "
"learns what 'normal' looks like and flags deviations from it.\n"
"- See the **Model Evaluation** tab for the feature-distribution chart "
"showing exactly how normal vs. anomalous readings differ, plus "
"precision/recall/F1/ROC-AUC on held-out data."
)
preset_dropdown = gr.Dropdown(
choices=list(ANOMALY_PRESETS.keys()), label="Load a preset reading", value="Normal reading"
)
with gr.Row():
motor_temp_in = gr.Slider(30, 100, value=55, step=0.5, label="Motor temperature (°C)")
vibration_in = gr.Slider(0, 10, value=2.2, step=0.1, label="Vibration (mm/s)")
with gr.Row():
current_in = gr.Slider(5, 30, value=12, step=0.5, label="Motor current (A)")
belt_speed_in = gr.Slider(0.1, 2.5, value=1.5, step=0.05, label="Belt speed (m/s)")
check_btn = gr.Button("Check for anomaly", variant="primary")
anomaly_output = gr.Markdown()
preset_dropdown.change(
load_preset, inputs=preset_dropdown,
outputs=[motor_temp_in, vibration_in, current_in, belt_speed_in],
)
check_btn.click(
anomaly_fn,
inputs=[motor_temp_in, vibration_in, current_in, belt_speed_in],
outputs=anomaly_output,
)
with gr.Tab("📊 Model Evaluation"):
gr.Markdown(
"Every metric on this tab is computed on **held-out test data** by "
"`build_artifacts.py` (not cherry-picked from a live demo run) -- "
"re-run that script any time to reproduce these numbers from scratch."
)
gr.Markdown(eval_intent_section_md())
gr.Markdown("**Dataset composition** (how many training examples per intent):")
gr.Image(os.path.join(ASSETS_DIR, "intent_dataset_composition.png"), show_label=False, container=False)
gr.Markdown("**Per-class precision / recall / F1:**")
gr.Image(os.path.join(ASSETS_DIR, "intent_per_class_bar.png"), show_label=False, container=False)
gr.Markdown("**Confusion matrix:**")
gr.Image(os.path.join(ASSETS_DIR, "intent_confusion_matrix.png"), show_label=False, container=False)
gr.Markdown("---")
gr.Markdown(eval_anomaly_section_md())
gr.Markdown("**Sensor feature distributions (normal vs. anomaly):**")
gr.Image(os.path.join(ASSETS_DIR, "sensor_distributions.png"), show_label=False, container=False)
with gr.Row():
with gr.Column():
gr.Markdown("**Evaluation metrics:**")
gr.Image(os.path.join(ASSETS_DIR, "anomaly_metrics_bar.png"), show_label=False, container=False)
with gr.Column():
gr.Markdown("**Confusion matrix:**")
gr.Image(os.path.join(ASSETS_DIR, "anomaly_confusion_matrix.png"), show_label=False, container=False)
gr.Markdown("**ROC curve:**")
gr.Image(os.path.join(ASSETS_DIR, "anomaly_roc_curve.png"), show_label=False, container=False)
gr.Markdown("---")
gr.Markdown(eval_retrieval_section_md())
gr.Image(os.path.join(ASSETS_DIR, "retrieval_hitrate_bar.png"), show_label=False, container=False)
gr.Markdown("---")
gr.Markdown(eval_latency_section_md())
gr.Image(os.path.join(ASSETS_DIR, "latency_bar.png"), show_label=False, container=False)
gr.Markdown("---")
gr.Markdown(EVAL_METHODOLOGY_MD)
with gr.Tab("ℹ️ About"):
gr.Markdown(ABOUT_MD)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))