Haseeb949/fluenta-backend
0
1"""2predict_local.py3 4Standalone script to predict a single WAV file using retrained artifacts.5 6Usage:7 python predict_local.py # uses default test file (/mnt/data/fluent_1.wav)8 python predict_local.py path/to/file.wav9"""10import sys11from pathlib import Path12import joblib13import numpy as np14import soundfile as sf15import librosa16 17# Configuration: path to retrained artifacts (matches retrain output)18MODEL_DIR = Path(r"D:/fluenta_fresh/backend/backend/models_retrained")19MODEL_FILE = "stutter_model_retrained.pkl"20SCALER_FILE = "scaler_retrained.pkl"21FCOUNT_FILE = "feature_count.pkl"22 23THRESH = 0.8 # threshold for labeling stutter24 25# ---------- Feature extraction (same as app.py) ----------26def preprocess_audio(y: np.ndarray, sr: int, target_sr: int = 16000):27 # 1. Energy check28 if len(y) == 0: return y, sr29 rms = np.sqrt(np.mean(y**2))30 duration = len(y) / sr31 if duration < 1.0 or rms < 0.01: return np.array([]), sr32 33 34 35 36 # 2. Trim37 try:38 y_trimmed, _ = librosa.effects.trim(y, top_db=30)39 if len(y_trimmed) >= int(0.1 * sr): y = y_trimmed40 except Exception: pass41 42 # 3. Normalize43 try: y = librosa.util.normalize(y)44 except Exception: pass45 46 # 4. Resample47 if sr != target_sr:48 try:49 y = librosa.resample(y, orig_sr=sr, target_sr=target_sr)50 sr = target_sr51 except Exception: pass52 53 # 5. Padding54 min_length = int(0.5 * sr)55 if 0 < len(y) < min_length:56 y = np.pad(y, (0, max(0, min_length - len(y))), mode="constant")57 return y, sr58 59 60def extract_60_features(path: str):61 # Use librosa.load for better robustness (supports more formats than soundfile)62 y, sr = librosa.load(path, sr=16000)63 y, sr = preprocess_audio(y, sr, target_sr=16000)64 65 if len(y) == 0:66 raise ValueError("Audio is too quiet or silent. Please provide a clearer recording.")67 68 feats = []69 mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20)70 feats.extend(np.mean(mfccs.T, axis=0).tolist())71 feats.extend(np.std(mfccs.T, axis=0).tolist())72 spectral_centroids = librosa.feature.spectral_centroid(y=y, sr=sr)[0]73 feats.append(float(np.mean(spectral_centroids)))74 feats.append(float(np.std(spectral_centroids)))75 spectral_rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr)[0]76 feats.append(float(np.mean(spectral_rolloff)))77 feats.append(float(np.std(spectral_rolloff)))78 zcr = librosa.feature.zero_crossing_rate(y)[0]79 feats.append(float(np.mean(zcr)))80 feats.append(float(np.std(zcr)))81 chroma = librosa.feature.chroma_stft(y=y, sr=sr)82 feats.extend(np.mean(chroma.T, axis=0).tolist())83 rms = librosa.feature.rms(y=y)[0]84 feats.append(float(np.mean(rms)))85 feats.append(float(np.std(rms)))86 arr = np.asarray(feats, dtype=np.float32)87 if arr.size != 60:88 raise ValueError(f"Extracted feature length {arr.size} != expected 60")89 return arr90 91# ---------- Load artifacts ----------92def load_artifacts(model_dir: Path = MODEL_DIR):93 model_path = model_dir / MODEL_FILE94 scaler_path = model_dir / SCALER_FILE95 fcount_path = model_dir / FCOUNT_FILE96 if not model_path.exists() or not scaler_path.exists() or not fcount_path.exists():97 raise FileNotFoundError(f"Missing model/scaler/feature_count in {model_dir}")98 model = joblib.load(model_path)99 scaler = joblib.load(scaler_path)100 fcount = joblib.load(fcount_path)101 try:102 fcount = int(fcount)103 except Exception:104 fcount = int(np.asarray(fcount).item())105 return model, scaler, fcount106 107def predict_file(model, scaler, features: np.ndarray, thresh: float = THRESH):108 X_scaled = scaler.transform([features])109 raw_pred = int(model.predict(X_scaled)[0])110 p_stutter = None111 if hasattr(model, "predict_proba"):112 raw_proba = model.predict_proba(X_scaled)[0]113 idx = 1 if len(raw_proba) > 1 else 0114 if hasattr(model, "classes_"):115 classes = list(model.classes_)116 if 1 in classes:117 idx = classes.index(1)118 if 0 <= idx < len(raw_proba):119 p_stutter = float(raw_proba[idx])120 # decide label121 if p_stutter is not None:122 if p_stutter >= thresh:123 label = "Stutter"124 confidence = p_stutter125 else:126 label = "Non-Stutter"127 confidence = 1.0 - p_stutter128 else:129 label = "Stutter" if raw_pred == 1 else "Non-Stutter"130 confidence = None131 return raw_pred, confidence, label, p_stutter132 133# ---------- Main ----------134def main():135 # default test file (uploaded earlier)136 default_fp = Path("/mnt/data/fluent_1.wav")137 fp = Path(sys.argv[1]) if len(sys.argv) > 1 else default_fp138 if not fp.exists():139 print("File not found:", fp)140 return141 model, scaler, fcount = load_artifacts(MODEL_DIR)142 print("Loaded model from:", MODEL_DIR)143 feats = extract_60_features(str(fp))144 raw_pred, confidence, label, p_stutter = predict_file(model, scaler, feats, thresh=THRESH)145 print("=== Prediction ===")146 print("File:", fp)147 print("Raw predicted class:", raw_pred)148 if p_stutter is not None:149 print(f"Probability(stutter): {p_stutter:.4f}")150 print(f"Used threshold: {THRESH}")151 print("Final label:", label)152 if confidence is not None:153 print(f"Confidence (used for label): {confidence*100:.2f}%")154 else:155 print("Confidence: not available (model.predict_proba missing)")156 print("==================")157 158if __name__ == "__main__":159 main()160 