CoolFace
Apppublic

Haseeb949/fluenta-backend

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
feature_extraction.py108 linesDownload Raw Back to root
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