""" Smart Warehouse AI Assistant ============================= A warehouse/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 a portfolio / job-application project. """ 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") anomaly_threshold_path = os.path.join(MODELS_DIR, "anomaly_threshold.joblib") def _try_load(): pipeline = load_pipeline(intent_path) model, scaler, threshold = load_anomaly_artifacts(anomaly_model_path, anomaly_scaler_path, threshold_path=anomaly_threshold_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}, threshold=threshold) return pipeline, model, scaler, threshold 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, anomaly_threshold = _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 status of order #10007?", "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?", "Route AGV-12 to picking station 5", "What is cycle counting?", "Redirect AGV-07 around the blocked aisle in Zone B", "How does an AS/RS crane retrieve a pallet?", "What KPIs matter most in warehouse automation?", "Schedule maintenance for Sorter-02", "Should we batch pick these orders together?", "What is predictive maintenance?", "Log a safety incident involving AMR-21", ] # ========================================================================== # 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, threshold=anomaly_threshold) 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(): return ( f"### 1. Intent Classifier   " f"🎯 **{intent_eval.get('accuracy', 0):.0%} accuracy**  ·  " f"**{intent_eval.get('macro_f1', 0):.0%} macro F1**  ·  " f"held-out test set, {intent_eval.get('n_test', '?')} examples, " f"{intent_eval.get('n_classes', '?')} classes" ) def eval_anomaly_section_md(): return ( f"### 2. Predictive Maintenance (Anomaly Detector)   " f"🎯 **{anomaly_eval.get('f1', 0):.0%} F1**  ·  " f"**{anomaly_eval.get('roc_auc', 0):.2f} ROC-AUC**  ·  " f"**{anomaly_eval.get('precision', 0):.0%} precision**  ·  " f"**{anomaly_eval.get('recall', 0):.0%} recall**" ) def eval_retrieval_section_md(): return ( f"### 3. RAG Retrieval   " f"🎯 **{retrieval_eval.get('hit_rate_at_1', 0):.0%} hit-rate@1**  ·  " f"**{retrieval_eval.get('hit_rate_at_2', 0):.0%} hit-rate@2**  ·  " f"{retrieval_eval.get('n_queries', '?')} labelled test queries" ) def eval_latency_section_md(): return ( f"### 4. Latency (CPU, avg of 50 runs)   " f"🎯 **{latency_eval.get('intent_classifier_ms', '?')} ms** intent  ·  " f"**{latency_eval.get('anomaly_detector_ms', '?')} ms** anomaly  ·  " f"**{latency_eval.get('kb_retrieval_ms', '?')} ms** retrieval" ) EVAL_METHODOLOGY_MD = ( "*All metrics above are computed on held-out synthetic test data by " "`build_artifacts.py` (fully reproducible) — see the **About** tab for " "dataset details and how each model works. Two safeguards keep these " "numbers honest: (1) train/test splits are deduplicated on exact text " "with an automated zero-overlap check, so the intent classifier can't " "memorize verbatim test examples; (2) the anomaly detector is fit " "unsupervised on a training split, then its decision threshold is " "calibrated on a separate small *labelled calibration split* (analogous " "to a handful of confirmed historical incidents) — never on the test " "set used to report the metrics above.*" ) # ========================================================================== # TAB 5 -- About # ========================================================================== ABOUT_MD = f""" # 🏭 Smart Warehouse AI Assistant **A portfolio project demonstrating an applied-AI approach to intralogistics and warehouse automation operations.** Modern warehouse automation platforms — automated storage & retrieval systems (AS/RS), conveyor & sortation lines, AGVs/AMRs, and the WMS/WCS software that orchestrates them — generate huge volumes of operational data: equipment telemetry, transactions, safety logs, and ad-hoc questions from floor staff." ## What this demonstrates | Capability | Where | Techniques used | |---|---|---| | **LLM-powered natural-language assistant**, grounded with retrieval (RAG) so it answers from real domain knowledge rather than hallucinating | *AI Assistant* tab | TF-IDF retrieval + hosted LLM (HF Inference API) | | **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 | TF-IDF + Logistic Regression | | **Predictive maintenance** via unsupervised anomaly detection on conveyor/crane sensor streams — catching bearing wear or misalignment before an unplanned stoppage | *Predictive Maintenance* tab | Isolation Forest, unsupervised | | **NL-to-structured-query** over inventory/order data, a lightweight stand-in for a WMS query tool | *Inventory & Order Query* tab | Regex slot extraction + intent routing | | **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 | scikit-learn metrics, held-out test splits | ## Dataset overview — what the data is and where it's from **Every dataset in this project is synthetically generated by the project's own code** (`src/data_generation.py` and `src/knowledge_base.py`), not scraped, exported, or sourced from any real company's systems. This was a deliberate choice: it keeps the project fully self-contained, reproducible, and shareable without any data-privacy or licensing concerns, while still being realistic enough to demonstrate the underlying ML techniques properly. | Dataset | What it is | Size | How it's generated | |---|---|---|---| | **Knowledge base** | Original, hand-written articles on generic warehouse-automation concepts (AS/RS, AGV/AMR, WMS, sortation, picking strategy, predictive maintenance, safety, inventory accuracy, KPIs, energy efficiency) | 10 articles | Written by the developer specifically for this project | | **Intent queries** | Example free-text operational questions/requests across 8 categories | ~480 examples | ~8 templates per category with randomised SKU codes, zone names, order IDs, and equipment IDs slotted in | | **Inventory table** | SKU records with category, zone, on-hand units, reorder point, unit cost | 60 SKUs | Randomised within realistic ranges (fixed seed) | | **Orders table** | Order records with status, line count, priority, zone | 80 orders | Randomised within realistic ranges (fixed seed) | | **Sensor readings** | Conveyor/crane motor telemetry: temperature, vibration, current, belt speed | 1,000 readings (900 normal + 100 anomalous) | Normal readings drawn from realistic operating ranges; anomalies simulate known failure signatures (elevated temp/vibration/current + reduced belt speed) | | **Retrieval eval set** | Hand-labelled (question → expected knowledge-base article) pairs | 10 pairs | Written by the developer to check retrieval accuracy | ## How each model works (function) | Model | Purpose | Algorithm | Input → Output | |---|---|---|---| | **Intent classifier** | Decide what kind of request a query is (maintenance, safety, inventory, order status, navigation, picking, system status, general FAQ) | TF-IDF + Logistic Regression | Free text → intent label + confidence | | **KB retriever** | Find the most relevant knowledge-base passage(s) for a query, to ground the LLM's answer | TF-IDF + cosine similarity | Free text → top-k ranked passages | | **LLM assistant** | Generate a natural-language answer grounded in the retrieved context | Hosted instruct LLM (HF Inference API), multi-model fallback chain | Query + context → grounded answer (or an extractive fallback if the LLM is unavailable) | | **Inventory/order query** | Turn a question into a filtered table lookup | Regex slot extraction (SKU / order ID / zone) + intent routing | Free text → filtered inventory or orders table | | **Anomaly detector** | Flag abnormal equipment sensor readings before they cause a stoppage | Isolation Forest (unsupervised), StandardScaler | 4 sensor features → anomaly / normal + anomaly score | ## How to test this project 1. **AI Assistant** — try one of the example questions, or ask your own (e.g. *"The conveyor belt in Zone C is making noise"*). Check the metadata line under each answer to see the detected intent, which knowledge-base article(s) were retrieved, and whether the LLM or the fallback path answered. If the LLM path isn't working, open the **LLM connection diagnostics** accordion and click "Test LLM connection" for a precise error message. 2. **Inventory & Order Query** — try *"How many units of SKU-1042 are in Zone B?"* or *"What's the status of order #10007?"*, or browse the full synthetic tables in the accordion below the search box. 3. **Predictive Maintenance** — load one of the presets (Normal / Early bearing wear / Severe fault) or drag the sliders yourself, then click *"Check for anomaly"* to see the model's verdict and recommended action. 4. **Model Evaluation** — every chart here is generated on **held-out test data** by `build_artifacts.py`, not cherry-picked from a live run. Run that script yourself to reproduce every number from scratch. ## 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`), with a multi-model fallback chain and auto provider routing. Configurable via the `LLM_MODEL_ID` env var. Falls back gracefully to a retrieval-only answer if no API token is configured or every candidate model fails, so the public demo never just 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, self-healing if the deployed scikit-learn version ever drifts from the one used to train the models). ## 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 approach The value of AI in a warehouse-automation context 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. Every component here was chosen to be as simple as it can be while still doing that job well and being honestly evaluated, rather than reaching for the biggest available model by default. ## 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 a portfolio/application project. Source code available on request or 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; } .data-badge { background: #eff6ff; border-left: 4px solid #3b82f6; border-radius: 6px; padding: 10px 14px; margin-bottom: 10px; font-size: 0.92em; line-height: 1.5; } .data-badge b { color: #1e3a8a; } """ def data_badge(html: str) -> str: return f'
{html}
' with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), css=CUSTOM_CSS, title="Smart Warehouse AI Assistant") as demo: gr.Markdown( "

🏭 Smart Warehouse AI Assistant

" "

LLM-powered intralogistics copilot · " "intent routing · predictive maintenance · retrieval-grounded Q&A

" ) 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"): with gr.Accordion("ℹ️ About this AI Assistant & Data Source", open=False): gr.HTML(data_badge( "🧪 Source: hand-written by the developer — 10 original knowledge-base " "articles on warehouse operations (AS/RS, AGV/AMR, WMS, sortation, picking, " "maintenance, safety) plus ~480 template-generated example queries. " "Not scraped or sourced from any real company.
" "What it describes: generic intralogistics/automation concepts and " "operational scenarios (equipment faults, safety incidents, order/inventory " "questions) — general domain knowledge, not any specific facility's live data.
" "Function: answers free-text warehouse-ops questions by first classifying " "the query's intent, retrieving the most relevant knowledge-base passage(s) " "(RAG), then generating a grounded answer with a hosted LLM — falling back to " "showing the retrieved passages directly if no LLM is available." )) 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"): with gr.Accordion("ℹ️ About Data & Query System", open=False): gr.HTML(data_badge( f"🧪 Source: randomly generated by the developer's code " f"(src/data_generation.py, fixed seed) — " f"{len(inventory_df)} synthetic SKU records and " f"{len(orders_df)} synthetic orders. " "Not exported from any real company's WMS.
" "What it describes: a stand-in inventory table " "(SKU, category, zone, on-hand units, reorder point, unit cost) " "and orders table (order ID, status, line count, priority, zone) — " "realistic in shape and ranges, but fictional records, not live " "warehouse data.
" "Function: classifies whether a query is about inventory or " "order status, extracts SKU/order-ID/zone identifiers with regex, " "and filters the corresponding table — a lightweight stand-in " "for a natural-language WMS query tool." )) with gr.Row(): inv_input = gr.Textbox(label="Query", placeholder="How many units of SKU-1042 are in Zone C?", 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 C?", "What's the status of order #10007?", "Show me low stock items", "Any delayed orders?", "Is SKU-1015 in stock at Zone D?", "Do we have enough SKU-1030 to fulfill 200 units?", "Track order #10021 for me", "Show the fulfillment status of #10045", ], 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"): with gr.Accordion("ℹ️ About Data & Predictive System", open=False): gr.HTML(data_badge( "🧪 Source: randomly generated by the developer's code " "(`src/data_generation.py`, fixed seed) — 1,000 synthetic sensor readings " "(900 normal + 100 simulated-fault). Not logged from real equipment.
" "What it describes: conveyor/crane motor sensor readings — temperature, " "vibration, current draw, and belt speed — with the 'anomaly' readings modelled " "on realistic failure signatures (elevated temp/vibration/current with reduced " "belt speed, as seen with bearing wear or motor overload).
" "Function: an Isolation Forest model trained unsupervised (it never sees " "an anomaly label) learns what 'normal' sensor behaviour looks like, then flags " "any new reading — like the ones you enter below — that deviates from it, as an " "early-warning signal before an unplanned equipment stoppage." )) 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"): with gr.Accordion("ℹ️ About Model Evaluation", open=False): gr.HTML(data_badge( "🧪 All charts below are computed on held-out test data " "by build_artifacts.py — fully reproducible." )) gr.Markdown("### 1. Intent Classifier — accuracy: **{:.1%}** · macro F1: **{:.1%}**".format( intent_eval.get("accuracy", 0), intent_eval.get("macro_f1", 0) )) with gr.Row(): gr.Image(os.path.join(ASSETS_DIR, "intent_dataset_composition.png"), show_label=False, container=False) gr.Image(os.path.join(ASSETS_DIR, "intent_per_class_bar.png"), show_label=False, container=False) gr.Image(os.path.join(ASSETS_DIR, "intent_confusion_matrix.png"), show_label=False, container=False) gr.Markdown("### 2. Predictive Maintenance — F1: **{:.1%}** · ROC-AUC: **{:.2f}**".format( anomaly_eval.get("f1", 0), anomaly_eval.get("roc_auc", 0) )) gr.Image(os.path.join(ASSETS_DIR, "sensor_distributions.png"), show_label=False, container=False) with gr.Row(): gr.Image(os.path.join(ASSETS_DIR, "anomaly_metrics_bar.png"), show_label=False, container=False) gr.Image(os.path.join(ASSETS_DIR, "anomaly_confusion_matrix.png"), show_label=False, container=False) gr.Image(os.path.join(ASSETS_DIR, "anomaly_roc_curve.png"), show_label=False, container=False) gr.Markdown("### 3. RAG Retrieval — Hit-rate@1: **{:.0%}** · Hit-rate@2: **{:.0%}**".format( retrieval_eval.get("hit_rate_at_1", 0), retrieval_eval.get("hit_rate_at_2", 0) )) gr.Image(os.path.join(ASSETS_DIR, "retrieval_hitrate_bar.png"), show_label=False, container=False) gr.Markdown("### 4. Latency Benchmark (CPU, local components)") gr.Image(os.path.join(ASSETS_DIR, "latency_bar.png"), show_label=False, container=False) with gr.Accordion("📋 Full metrics tables & methodology notes", open=False): gr.Markdown(eval_intent_section_md()) gr.Markdown(eval_anomaly_section_md()) gr.Markdown(eval_retrieval_section_md()) gr.Markdown(eval_latency_section_md()) gr.Markdown(EVAL_METHODOLOGY_MD) with gr.Tab("ℹ️ About"): gr.Markdown(ABOUT_MD) if __name__ == "__main__": # ssr_mode=False: Gradio's experimental server-side rendering mode was # causing repeated "SvelteKitError: POST method not allowed" / 405 log # spam on Spaces (some requests hit the SSR page route instead of the # API route). Disabling it avoids that; this app has no functional need # for SSR. Wrapped defensively in case the kwarg name differs across # Gradio versions -- falls back to a plain launch() rather than crashing # the Space if so. launch_kwargs = dict(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860))) try: demo.launch(**launch_kwargs, ssr=False) except TypeError: demo.launch(**launch_kwargs)