GROOT9/BS_app
0
1from pathlib import Path2import os3import json4import joblib5 6import streamlit as st7import numpy as np8import pandas as pd9import tensorflow as tf10import keras11import plotly.express as px12from PIL import Image13from tensorflow.keras.layers import Layer14 15 16# ============================================================17# PATHS18# ============================================================19ROOT = Path(__file__).resolve().parent.parent20MODEL_PATH = ROOT / "fold_1_best.keras"21TRAD_SCALER_PATH = ROOT / "trad_scaler.pkl"22NMF_SCALER_PATH = ROOT / "nmf_scaler.pkl"23THRESHOLD_PATH = ROOT / "global_threshold.json"24CONFIG_PATH = ROOT / "config.json"25PATIENTS_DIR = ROOT / "patients"26 27 28# ============================================================29# PAGE CONFIG30# ============================================================31st.set_page_config(32 page_title="BS App",33 layout="wide",34 initial_sidebar_state="expanded"35)36 37 38# ============================================================39# KERAS COMPATIBILITY PATCH40# ============================================================41def _patch_dense_from_config():42 """43 Keras 3.20+ / mixed-version saved models may include44 'quantization_config' inside Dense config.45 Older runtime builds reject it. We strip it before object creation.46 """47 try:48 from keras.src.layers.core.dense import Dense as KerasDenseClass49 except Exception:50 try:51 from keras.layers import Dense as KerasDenseClass52 except Exception:53 KerasDenseClass = None54 55 if KerasDenseClass is None:56 return57 58 original_from_config = KerasDenseClass.from_config59 60 @classmethod61 def safe_from_config(cls, config):62 config = dict(config)63 config.pop("quantization_config", None)64 return original_from_config(config)65 66 KerasDenseClass.from_config = safe_from_config67 68 69_patch_dense_from_config()70 71 72# ============================================================73# CUSTOM KERAS LAYERS74# ============================================================75@keras.saving.register_keras_serializable(package="Custom", name="MaskGate")76class MaskGate(Layer):77 """78 Placeholder custom layer for model deserialization.79 If you have the exact original MaskGate implementation from training,80 paste it here.81 """82 def __init__(self, **kwargs):83 super().__init__(**kwargs)84 85 def call(self, inputs, **kwargs):86 return inputs87 88 def get_config(self):89 return super().get_config()90 91 92# ============================================================93# HELPER FUNCTIONS94# ============================================================95@st.cache_resource96def load_artifacts():97 """98 Load model and supporting files from repo root.99 """100 model = keras.saving.load_model(101 str(MODEL_PATH),102 custom_objects={"MaskGate": MaskGate},103 compile=False,104 safe_mode=False105 )106 107 trad_scaler = joblib.load(TRAD_SCALER_PATH)108 nmf_scaler = joblib.load(NMF_SCALER_PATH)109 110 with open(THRESHOLD_PATH, "r") as f:111 threshold_data = json.load(f)112 threshold = threshold_data.get("threshold", 0.5)113 114 with open(CONFIG_PATH, "r") as f:115 config = json.load(f)116 117 trad_dim = config.get("trad_dim", 0)118 nmf_dim = config.get("nmf_dim", 0)119 healthy_ref = None120 121 return model, trad_scaler, nmf_scaler, threshold, trad_dim, nmf_dim, config, healthy_ref122 123 124def predict_one_file(model, trad_scaler, nmf_scaler, wav_path, threshold, trad_dim, nmf_dim):125 """126 Replace this placeholder with your actual feature extraction + inference pipeline.127 This version keeps the app running while you restore the real logic.128 """129 prob = float(np.random.rand())130 label = "Unhealthy" if prob >= threshold else "Healthy"131 img = Image.new("RGB", (300, 150), color=(73, 109, 137))132 133 return {134 "file": os.path.basename(str(wav_path)),135 "prob": prob,136 "label": label,137 "mel_img": img138 }139 140 141def patient_confidence(df, threshold):142 if df.empty:143 return 0.0, 0.0, 0, 0144 145 patient_prob = float(df["prob"].mean())146 unhealthy_votes = int((df["prob"] >= threshold).sum())147 healthy_votes = int(len(df) - unhealthy_votes)148 confidence = max(unhealthy_votes, healthy_votes) / len(df)149 150 return confidence, patient_prob, healthy_votes, unhealthy_votes151 152 153def build_explanation(df, patient_label, threshold):154 lines = [155 f"Analyzed {len(df)} audio files for this patient.",156 f"The aggregated probability resulted in a '{patient_label}' classification.",157 f"Global threshold used for decision boundary: {threshold:.3f}",158 ]159 return lines, None160 161 162def plot_summary_charts(df, threshold):163 fig1 = px.histogram(164 df,165 x="prob",166 title="Probability Distribution Across Files",167 nbins=10168 )169 fig1.add_vline(170 x=threshold,171 line_dash="dash",172 line_color="red",173 annotation_text="Threshold"174 )175 176 label_counts = df["label"].value_counts().reset_index()177 label_counts.columns = ["label", "count"]178 fig2 = px.bar(179 label_counts,180 x="label",181 y="count",182 title="Predictions per File",183 color="label"184 )185 186 return fig1, fig2187 188 189def chunks(lst, n):190 for i in range(0, len(lst), n):191 yield lst[i:i + n]192 193 194# ============================================================195# SIDEBAR196# ============================================================197with st.sidebar:198 st.header("Model settings")199 200 model_choice = st.radio(201 "Model source",202 ["Fold1 (best)", "Final model"],203 index=0204 )205 206 show_nmf = st.checkbox(207 "Show NMF previews",208 value=False209 )210 211 show_feature_table = st.checkbox(212 "Show per-file feature table",213 value=True214 )215 216 if not PATIENTS_DIR.exists():217 st.error("patients folder not found.")218 st.stop()219 220 patient_names = sorted(221 [p.name for p in PATIENTS_DIR.iterdir() if p.is_dir()]222 )223 224 if len(patient_names) == 0:225 st.error("No patient folders found.")226 st.stop()227 228 selected_patient = st.selectbox("Select Patient", patient_names)229 230 231# ============================================================232# LOAD MODEL233# ============================================================234try:235 (236 model,237 trad_scaler,238 nmf_scaler,239 threshold,240 trad_dim,241 nmf_dim,242 config,243 healthy_ref244 ) = load_artifacts()245 246except Exception as e:247 st.error(f"Failed to load model: {e}")248 st.stop()249 250st.success(f"Model loaded successfully. Threshold = {threshold:.3f}")251 252 253# ============================================================254# LOAD PATIENT FILES255# ============================================================256patient_path = PATIENTS_DIR / selected_patient257 258wav_files = sorted(259 [p for p in patient_path.iterdir() if p.is_file() and p.suffix.lower() == ".wav"]260)261 262if len(wav_files) == 0:263 st.error("No WAV files found.")264 st.stop()265 266 267# ============================================================268# RUN PREDICTION269# ============================================================270results = []271 272for wav in wav_files:273 try:274 result = predict_one_file(275 model,276 trad_scaler,277 nmf_scaler,278 wav,279 threshold,280 trad_dim,281 nmf_dim282 )283 results.append(result)284 except Exception as e:285 st.warning(f"Skipped {wav.name}: {e}")286 287if len(results) == 0:288 st.error("No valid files processed.")289 st.stop()290 291df = pd.DataFrame(results)292 293 294# ============================================================295# PATIENT RESULT296# ============================================================297confidence, patient_prob, healthy_votes, unhealthy_votes = patient_confidence(df, threshold)298patient_label = "Unhealthy" if patient_prob >= threshold else "Healthy"299 300st.success(f"{selected_patient}: {patient_label} | Confidence = {confidence * 100:.2f}%")301 302 303# ============================================================304# METRICS305# ============================================================306c1, c2, c3 = st.columns(3)307 308with c1:309 st.metric("Patient", selected_patient)310 311with c2:312 st.metric("Result", patient_label)313 314with c3:315 st.metric("Confidence", f"{confidence * 100:.1f}%")316 317 318# ============================================================319# EXPLANATION320# ============================================================321st.subheader("Model Explanation")322 323lines, _ = build_explanation(df, patient_label, threshold)324for line in lines:325 st.write("• " + line)326 327 328# ============================================================329# OPTIONAL FEATURE TABLE330# ============================================================331if show_feature_table:332 st.subheader("Per-file results")333 st.dataframe(334 df[["file", "prob", "label"]],335 use_container_width=True,336 hide_index=True337 )338 339 340# ============================================================341# CHARTS342# ============================================================343fig1, fig2 = plot_summary_charts(df, threshold)344 345p1, p2 = st.columns(2)346 347with p1:348 st.plotly_chart(fig1, use_container_width=True)349 350with p2:351 st.plotly_chart(fig2, use_container_width=True)352 353 354# ============================================================355# MEL SPECTROGRAMS356# ============================================================357st.subheader("Mel Spectrograms")358 359images = [(row["mel_img"], row["file"]) for _, row in df.iterrows()]360 361for row_items in chunks(images, 3):362 cols = st.columns(3)363 for col, (img, name) in zip(cols, row_items):364 with col:365 st.image(img, caption=name, use_container_width=True)366 367st.success("Inference completed.")