CNN_space / app.py
shiko217's picture
fix
870d43b unverified
Raw
History Blame Contribute Delete
9.57 kB
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
import numpy as np
import librosa
import scipy.signal
from scipy.stats import kurtosis
import gradio as gr
import tensorflow as tf
import zipfile
import tempfile
import warnings
warnings.filterwarnings('ignore', category=UserWarning)
MAX_TIME_FRAMES = 313
N_MELS = 128
NUM_CLASSES = 6
LABEL_MAP_INVERSE = {
0: 'Machine 1_Normal', 1: 'Machine 1_Abnormal',
2: 'Machine 2_Normal', 3: 'Machine 2_Abnormal',
4: 'Machine 3_Normal', 5: 'Machine 3_Abnormal'
}
# ==========================================
# PREPROCESSOR
# ==========================================
class MachineListenerPreprocessor:
def __init__(self, target_sr=16000, n_fft=2048, hop_length=512, n_mels=N_MELS, n_mfcc=20):
self.sr = target_sr
self.n_fft = n_fft
self.hop_length = hop_length
self.n_mels = n_mels
self.n_mfcc = n_mfcc
def _apply_highpass_filter(self, y, cutoff=60.0):
nyquist = 0.5 * self.sr
normal_cutoff = cutoff / nyquist
if normal_cutoff >= 1.0:
return y
b, a = scipy.signal.butter(4, normal_cutoff, btype='high', analog=False)
return scipy.signal.filtfilt(b, a, y)
def _truncate_silence(self, y, top_db=25):
y_trimmed, _ = librosa.effects.trim(y, top_db=top_db, frame_length=self.n_fft, hop_length=self.hop_length)
return y_trimmed
def _mean_variance_normalize(self, y):
return (y - np.mean(y)) / (np.std(y) + 1e-8)
def process_audio(self, file_path):
y, _ = librosa.load(file_path, sr=self.sr)
y = self._apply_highpass_filter(y)
y = self._truncate_silence(y, top_db=25)
if len(y) == 0:
raise ValueError(f"Silence only: {file_path}")
y = self._mean_variance_normalize(y)
mel_spec = librosa.feature.melspectrogram(
y=y, sr=self.sr, n_fft=self.n_fft, hop_length=self.hop_length, n_mels=self.n_mels
)
log_mel_spec = librosa.power_to_db(mel_spec, ref=np.max)
return log_mel_spec
def pad_or_truncate(spectrogram, max_frames):
if spectrogram.shape[1] > max_frames:
return spectrogram[:, :max_frames]
elif spectrogram.shape[1] < max_frames:
pad_width = max_frames - spectrogram.shape[1]
return np.pad(spectrogram, pad_width=((0, 0), (0, pad_width)), mode='constant')
return spectrogram
# ==========================================
# CUSTOM LAYERS (replace Lambda layers from training)
# ==========================================
class NormLayer(tf.keras.layers.Layer):
"""Replaces Lambda(lambda t: t / 80.0) used for spectrogram normalization."""
def call(self, x):
return x / 80.0
class FreqReduceLayer(tf.keras.layers.Layer):
"""Replaces Lambda(lambda t: tf.reduce_mean(t, axis=1)) used to collapse frequency."""
def call(self, x):
return tf.reduce_mean(x, axis=1)
class SpecAugmentLayer(tf.keras.layers.Layer):
"""Training-only augmentation. Passes through during inference."""
def __init__(self, freq_mask_param=15, time_mask_param=30, **kwargs):
super().__init__(**kwargs)
self.freq_mask_param = freq_mask_param
self.time_mask_param = time_mask_param
def call(self, inputs, training=None):
if not training:
return inputs
freq_max = tf.shape(inputs)[1]
time_max = tf.shape(inputs)[2]
f = tf.random.uniform([], minval=0, maxval=self.freq_mask_param, dtype=tf.int32)
f0 = tf.random.uniform([], minval=0, maxval=freq_max - f, dtype=tf.int32)
freq_indices = tf.range(freq_max)
freq_mask = tf.logical_or(freq_indices < f0, freq_indices >= f0 + f)
freq_mask = tf.cast(freq_mask, inputs.dtype)
freq_mask = tf.reshape(freq_mask, [1, -1, 1, 1])
inputs = inputs * freq_mask
t = tf.random.uniform([], minval=0, maxval=self.time_mask_param, dtype=tf.int32)
t0 = tf.random.uniform([], minval=0, maxval=time_max - t, dtype=tf.int32)
time_indices = tf.range(time_max)
time_mask = tf.logical_or(time_indices < t0, time_indices >= t0 + t)
time_mask = tf.cast(time_mask, inputs.dtype)
time_mask = tf.reshape(time_mask, [1, 1, -1, 1])
inputs = inputs * time_mask
return inputs
def get_config(self):
config = super().get_config()
config.update({
"freq_mask_param": self.freq_mask_param,
"time_mask_param": self.time_mask_param,
})
return config
# ==========================================
# MODEL ARCHITECTURE (exact replica of V2-F)
# ==========================================
def se_block(x, filters, ratio=8):
se = tf.keras.layers.GlobalAveragePooling2D()(x)
se = tf.keras.layers.Dense(filters // ratio, activation='relu')(se)
se = tf.keras.layers.Dense(filters, activation='sigmoid')(se)
se = tf.keras.layers.Reshape([1, 1, filters])(se)
return x * se
def build_v2f():
inp = tf.keras.Input(shape=(N_MELS, MAX_TIME_FRAMES, 1))
x = SpecAugmentLayer(freq_mask_param=15, time_mask_param=30)(inp)
x = NormLayer()(x)
# Block 1
x = tf.keras.layers.Conv2D(32, (3, 3), padding='same',
kernel_regularizer=tf.keras.regularizers.l2(1e-4))(x)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.Activation('relu')(x)
x = se_block(x, 32)
x = tf.keras.layers.MaxPooling2D(pool_size=(2, 1))(x)
x = tf.keras.layers.Dropout(0.2)(x)
# Block 2
x = tf.keras.layers.Conv2D(64, (3, 3), padding='same',
kernel_regularizer=tf.keras.regularizers.l2(1e-4))(x)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.Activation('relu')(x)
x = se_block(x, 64)
x = tf.keras.layers.MaxPooling2D(pool_size=(2, 1))(x)
x = tf.keras.layers.Dropout(0.2)(x)
# Block 3
x = tf.keras.layers.Conv2D(128, (3, 3), padding='same',
kernel_regularizer=tf.keras.regularizers.l2(1e-4))(x)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.Activation('relu')(x)
x = se_block(x, 128)
x = tf.keras.layers.MaxPooling2D(pool_size=(2, 1))(x)
x = tf.keras.layers.Dropout(0.2)(x)
# Bridge: reduce channels and collapse frequency
x = tf.keras.layers.Conv2D(64, (1, 1), padding='same', activation='relu')(x)
x = FreqReduceLayer()(x)
# BiLSTM
x = tf.keras.layers.Bidirectional(
tf.keras.layers.LSTM(64, return_sequences=True)
)(x)
x = tf.keras.layers.Dropout(0.3)(x)
# MultiHead Attention
x = tf.keras.layers.MaxPooling1D(pool_size=4)(x)
x = tf.keras.layers.MultiHeadAttention(num_heads=4, key_dim=32)(x, x)
x = tf.keras.layers.GlobalAveragePooling1D()(x)
x = tf.keras.layers.Dropout(0.4)(x)
out = tf.keras.layers.Dense(NUM_CLASSES, activation='softmax')(x)
return tf.keras.Model(inputs=inp, outputs=out)
# ==========================================
# LOAD MODEL (rebuild + weights only)
# ==========================================
def load_model():
"""
Rebuild the V2-F architecture in code, then load ONLY the weights
from the .keras file. This completely bypasses Lambda deserialization
and Python bytecode compatibility issues.
"""
keras_path = 'best_v2f_generalist.keras'
try:
model = build_v2f()
# .keras file is a zip; extract the weights h5 and load
with tempfile.TemporaryDirectory() as tmpdir:
with zipfile.ZipFile(keras_path, 'r') as z:
z.extract('model.weights.h5', tmpdir)
model.load_weights(os.path.join(tmpdir, 'model.weights.h5'))
print("Model rebuilt and weights loaded successfully.")
return model, ""
except Exception as e:
return None, str(e)
# ==========================================
# STARTUP
# ==========================================
model, model_error = load_model()
if model is None:
print(f"Warning: Could not load model. Error: {model_error}")
preprocessor = MachineListenerPreprocessor()
# ==========================================
# PREDICTION
# ==========================================
def predict(audio_filepath):
if model is None:
return f"Model not loaded properly. Error: {model_error}"
if audio_filepath is None:
return "Please upload an audio file."
try:
log_mel_spec = preprocessor.process_audio(audio_filepath)
spec = pad_or_truncate(log_mel_spec, MAX_TIME_FRAMES)
# Shape: (1, 128, 313, 1) — single channel spectrogram
spec_batch = spec[np.newaxis, ..., np.newaxis].astype(np.float32)
predictions = model(spec_batch, training=False).numpy()
predicted_class_idx = np.argmax(predictions, axis=-1)[0]
predicted_label = LABEL_MAP_INVERSE.get(predicted_class_idx, "Unknown")
confidence = float(np.max(predictions))
return f"Prediction: {predicted_label} (Confidence: {confidence:.2f})"
except Exception as e:
return f"Error processing file: {str(e)}"
# ==========================================
# GRADIO UI
# ==========================================
iface = gr.Interface(
fn=predict,
inputs=gr.Audio(type="filepath", label="Upload Machine Audio"),
outputs="text",
title="Machine Listener Diagnosis",
description="Upload a sound from a machine to predict whether it is Normal or Abnormal."
)
if __name__ == "__main__":
iface.launch()