| """ |
| Handles reading the dataset zip and turning audio files into MFCC features. |
| |
| This replaces notebook cells 1, 2, and 6. Your original notebook had two |
| versions of this logic: |
| |
| - Cells 1-2 used two separate variables, `normal_path` and `stutter_path`, |
| but both were set to the SAME zip file. That means "normal" and |
| "stutter" samples were both being read from one file and labeled |
| 0 / 1 in a way that didn't actually correspond to real folders — |
| a real bug, not just a style issue. |
| |
| - Cell 6 fixed this properly: it reads ONE dataset zip and assigns the |
| label by looking at the folder name inside the zip ("Stutter" vs |
| "Normal"). That's the correct approach, so this file is built on |
| cell 6's logic. |
| |
| AUDIO FILE SAFETY NOTE |
| ----------------------- |
| We only ever open zip entries in-memory via `zip_ref.open(...)` and hand |
| a file-like object to librosa — we never call `extractall()` and never |
| write attacker-controlled filenames to disk. That's important because |
| zip files can contain path-traversal ("zip slip") entries like |
| `../../etc/passwd`. If you ever change this code to extract files to |
| disk, sanitize each filename first (reject any entry whose resolved |
| path escapes the target directory) before writing it. |
| """ |
|
|
| import os |
| import zipfile |
|
|
| import librosa |
| import numpy as np |
| from tqdm import tqdm |
|
|
| from config import DURATION_SECONDS, N_MFCC |
|
|
|
|
| def extract_features(file_path_or_object): |
| """Load an audio file (path or file-like object) and return its mean MFCC vector.""" |
| try: |
| audio, sr = librosa.load(file_path_or_object, duration=DURATION_SECONDS) |
|
|
| if len(audio) == 0: |
| print(f"Warning: empty audio signal for {file_path_or_object}. Skipping.") |
| return None |
|
|
| mfcc = librosa.feature.mfcc(y=audio, sr=sr, n_mfcc=N_MFCC) |
| return np.mean(mfcc.T, axis=0) |
|
|
| except Exception as e: |
| print(f"Error extracting features from {file_path_or_object}: {e}") |
| return None |
|
|
|
|
| def build_dataset(dataset_path): |
| """ |
| Read the dataset zip, extract MFCC features for every audio file, and |
| label each sample by its folder name ("Stutter" -> 1, "Normal" -> 0). |
| |
| Returns (X, y) as numpy arrays. |
| """ |
| X, y = [], [] |
|
|
| if not os.path.exists(dataset_path): |
| raise FileNotFoundError(f"Dataset zip not found at: {dataset_path}") |
|
|
| with zipfile.ZipFile(dataset_path, "r") as zip_ref: |
| namelist = zip_ref.namelist() |
|
|
| folders = {n.split("/")[1] for n in namelist if "/" in n and len(n.split("/")) > 1} |
| print("Folders found in zip:", folders) |
|
|
| empty_count = 0 |
| for file_name in tqdm(namelist, desc="Processing Audio"): |
| if not file_name.endswith((".wav", ".mp3", ".flac", ".ogg")) or file_name.endswith("/"): |
| continue |
|
|
| if "Stutter" in file_name: |
| label = 1 |
| elif "Normal" in file_name: |
| label = 0 |
| else: |
| continue |
|
|
| with zip_ref.open(file_name) as audio_file_in_zip: |
| features = extract_features(audio_file_in_zip) |
|
|
| if features is not None: |
| X.append(features) |
| y.append(label) |
| else: |
| empty_count += 1 |
|
|
| print(f"Skipped {empty_count} empty/unreadable files") |
|
|
| X = np.array(X) |
| y = np.array(y) |
| print("Feature matrix:", X.shape) |
| print("Labels:", y.shape) |
| if len(y) > 0: |
| print("Class distribution:", np.bincount(y)) |
|
|
| return X, y |
|
|