File size: 3,568 Bytes
4bb1160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""
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  # skip files not in a recognized folder

            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