Haseeb949/fluenta-backend
0
1# feature_extraction.py2import numpy as np3import soundfile as sf4import librosa5import noisereduce as nr6import logging7from typing import Optional8 9logger = logging.getLogger("feature_extraction")10 11def preprocess_audio(y, sr, target_sr=16000):12 # 1. Basic energy check13 if len(y) == 0:14 return y, sr15 16 rms_energy = np.sqrt(np.mean(y**2))17 duration = len(y) / sr18 if duration < 1.0 or rms_energy < 0.01:19 20 # Return empty for too short or quiet audio21 return np.array([]), sr22 23 24 # 2. Trim silence with a slightly more relaxed threshold (30db instead of 20)25 try:26 y_trimmed, _ = librosa.effects.trim(y, top_db=30)27 if len(y_trimmed) >= int(0.1 * sr):28 y = y_trimmed29 except Exception:30 pass31 32 # 3. Normalize33 try:34 y = librosa.util.normalize(y)35 except Exception:36 pass37 38 # 4. Noise reduction39 try:40 y_denoised = nr.reduce_noise(y=y, sr=sr, prop_decrease=0.8)41 if not np.all(np.isfinite(y_denoised)):42 y_denoised = np.nan_to_num(y_denoised)43 y = y_denoised44 except Exception:45 pass46 47 # 5. Resample48 if sr != target_sr:49 try:50 y = librosa.resample(y, orig_sr=sr, target_sr=target_sr)51 sr = target_sr52 except Exception:53 pass54 55 # 6. Minimum length padding56 min_length = int(0.5 * sr)57 if 0 < len(y) < min_length:58 y = np.pad(y, (0, max(0, min_length - len(y))), mode="constant")59 60 return y, sr61 62 63def extract_features(path: str) -> Optional[np.ndarray]:64 """65 Extract exactly the 60 features used by the model:66 - MFCC (20) mean + std => 4067 - spectral centroid mean/std => 268 - spectral rolloff mean/std => 269 - zcr mean/std => 270 - chroma mean (12) => 1271 - rms mean/std => 272 Total = 6073 """74 try:75 # Use librosa.load for better robustness76 y, sr = librosa.load(path, sr=16000)77 y, sr = preprocess_audio(y, sr, target_sr=16000)78 79 if len(y) == 0:80 logger.error("Audio is too quiet or empty after preprocessing.")81 return None82 83 84 feats = []85 mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20)86 feats.extend(np.mean(mfccs.T, axis=0).tolist())87 feats.extend(np.std(mfccs.T, axis=0).tolist())88 89 spectral_centroids = librosa.feature.spectral_centroid(y=y, sr=sr)[0]90 feats.append(float(np.mean(spectral_centroids))); feats.append(float(np.std(spectral_centroids)))91 92 spectral_rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr)[0]93 feats.append(float(np.mean(spectral_rolloff))); feats.append(float(np.std(spectral_rolloff)))94 95 zcr = librosa.feature.zero_crossing_rate(y)[0]96 feats.append(float(np.mean(zcr))); feats.append(float(np.std(zcr)))97 98 chroma = librosa.feature.chroma_stft(y=y, sr=sr)99 feats.extend(np.mean(chroma.T, axis=0).tolist())100 101 rms = librosa.feature.rms(y=y)[0]102 feats.append(float(np.mean(rms))); feats.append(float(np.std(rms)))103 104 return np.array(feats, dtype=np.float32)105 except Exception as e:106 logger.exception("Failed to extract features for %s: %s", path, e)107 return None108 