File size: 8,071 Bytes
875e4af
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
"""
Dataset inspection script for Phase 1.

This script is written and ready to run, but has NOT been executed against
real data yet because no dataset is present under data/raw/ as of this
commit. It performs no assumptions about label schema beyond what it can
detect from the actual files it finds — if it can't determine something
(e.g. speaker ID), it reports that explicitly rather than guessing.

Usage (once data/raw/ is populated):
    python scripts/inspect_dataset.py --data-dir data/raw --out docs/dataset_report.json

What it checks:
  - file inventory (audio files, manifest/label files found)
  - per-file audio properties: sample rate, channels, duration, corruption
  - label schema detection (from any CSV/JSON manifest found)
  - class balance (if labels found)
  - duplicate detection (exact file hash + near-duplicate via audio hash)
  - speaker/session ID presence (from manifest columns, if any)
  - transcript / language metadata presence
"""

import argparse
import hashlib
import json
import sys
from pathlib import Path

AUDIO_EXTENSIONS = {".wav", ".flac", ".mp3", ".ogg", ".m4a"}
MANIFEST_EXTENSIONS = {".csv", ".json", ".jsonl", ".tsv"}


def find_files(data_dir: Path):
    audio_files, manifest_files, other_files = [], [], []
    for p in data_dir.rglob("*"):
        if not p.is_file():
            continue
        suffix = p.suffix.lower()
        if suffix in AUDIO_EXTENSIONS:
            audio_files.append(p)
        elif suffix in MANIFEST_EXTENSIONS:
            manifest_files.append(p)
        else:
            other_files.append(p)
    return audio_files, manifest_files, other_files


def file_hash(path: Path, chunk_size: int = 1 << 20) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        while chunk := f.read(chunk_size):
            h.update(chunk)
    return h.hexdigest()


def inspect_audio_file(path: Path):
    """Return dict of measured properties, or an error record. No guessing."""
    try:
        import soundfile as sf
    except ImportError:
        return {"path": str(path), "error": "soundfile not installed"}

    try:
        info = sf.info(str(path))
        return {
            "path": str(path),
            "sample_rate": info.samplerate,
            "channels": info.channels,
            "duration_sec": round(info.frames / info.samplerate, 4)
            if info.samplerate
            else None,
            "frames": info.frames,
            "format": info.format,
            "corrupted": False,
        }
    except Exception as e:
        return {"path": str(path), "corrupted": True, "error": str(e)}


def try_load_manifests(manifest_files):
    """Load any CSV/JSON manifests found and report their columns.
    Does not assume which column is the label — reports raw schema so a
    human (or a follow-up script, once we know the real schema) can decide.
    """
    reports = []
    for mf in manifest_files:
        entry = {"path": str(mf)}
        try:
            if mf.suffix.lower() == ".csv" or mf.suffix.lower() == ".tsv":
                import csv

                delim = "\t" if mf.suffix.lower() == ".tsv" else ","
                with open(mf, newline="", encoding="utf-8", errors="replace") as f:
                    reader = csv.reader(f, delimiter=delim)
                    rows = list(reader)
                entry["n_rows"] = max(0, len(rows) - 1)
                entry["columns"] = rows[0] if rows else []
                entry["sample_rows"] = rows[1:4]
            elif mf.suffix.lower() in (".json", ".jsonl"):
                with open(mf, encoding="utf-8", errors="replace") as f:
                    if mf.suffix.lower() == ".jsonl":
                        lines = [json.loads(l) for l in f.readlines()[:5] if l.strip()]
                        entry["sample_records"] = lines
                    else:
                        data = json.load(f)
                        entry["top_level_type"] = type(data).__name__
                        if isinstance(data, list) and data:
                            entry["columns_guess"] = (
                                list(data[0].keys())
                                if isinstance(data[0], dict)
                                else None
                            )
                            entry["n_records"] = len(data)
                            entry["sample_records"] = data[:3]
                        elif isinstance(data, dict):
                            entry["top_level_keys"] = list(data.keys())
        except Exception as e:
            entry["error"] = str(e)
        reports.append(entry)
    return reports


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--data-dir", type=Path, default=Path("data/raw"))
    ap.add_argument("--out", type=Path, default=Path("docs/dataset_report.json"))
    ap.add_argument(
        "--max-audio-inspect",
        type=int,
        default=None,
        help="Cap number of audio files to probe for sample rate/duration "
        "(useful for a quick pass on very large datasets). Default: all.",
    )
    args = ap.parse_args()

    if not args.data_dir.exists():
        print(
            f"ERROR: {args.data_dir} does not exist. Nothing to inspect. "
            f"Place the raw dataset there first.",
            file=sys.stderr,
        )
        sys.exit(1)

    audio_files, manifest_files, other_files = find_files(args.data_dir)

    report = {
        "data_dir": str(args.data_dir),
        "n_audio_files_found": len(audio_files),
        "n_manifest_files_found": len(manifest_files),
        "n_other_files_found": len(other_files),
        "manifest_files": [str(p) for p in manifest_files],
        "other_files_sample": [str(p) for p in other_files[:20]],
    }

    if not audio_files and not manifest_files:
        print(
            f"WARNING: no audio or manifest files found under {args.data_dir}. "
            f"Found {len(other_files)} other files. Nothing measured.",
        )

    # Manifest schema (no label assumptions)
    report["manifests"] = try_load_manifests(manifest_files)

    # Audio properties + corruption + duplicate check
    to_inspect = audio_files[: args.max_audio_inspect] if args.max_audio_inspect else audio_files
    audio_reports = [inspect_audio_file(p) for p in to_inspect]
    report["audio_files"] = audio_reports

    corrupted = [r for r in audio_reports if r.get("corrupted")]
    report["n_corrupted"] = len(corrupted)

    hashes = {}
    for p in to_inspect:
        try:
            h = file_hash(p)
            hashes.setdefault(h, []).append(str(p))
        except Exception:
            pass
    duplicates = {h: paths for h, paths in hashes.items() if len(paths) > 1}
    report["n_exact_duplicate_groups"] = len(duplicates)
    report["exact_duplicates_sample"] = dict(list(duplicates.items())[:5])

    # Sample rate / channel / duration distribution summary (only over successfully read files)
    ok = [r for r in audio_reports if not r.get("corrupted")]
    if ok:
        srs = sorted(set(r["sample_rate"] for r in ok if r.get("sample_rate") is not None))
        chans = sorted(set(r["channels"] for r in ok if r.get("channels") is not None))
        durations = [r["duration_sec"] for r in ok if r.get("duration_sec") is not None]
        report["sample_rates_found"] = srs
        report["channel_counts_found"] = chans
        if durations:
            durations_sorted = sorted(durations)
            n = len(durations_sorted)
            report["duration_stats_sec"] = {
                "n": n,
                "min": durations_sorted[0],
                "max": durations_sorted[-1],
                "mean": round(sum(durations_sorted) / n, 4),
                "median": durations_sorted[n // 2],
            }

    args.out.parent.mkdir(parents=True, exist_ok=True)
    with open(args.out, "w") as f:
        json.dump(report, f, indent=2)

    print(f"Wrote dataset report to {args.out}")
    print(json.dumps({k: v for k, v in report.items() if k != "audio_files"}, indent=2))


if __name__ == "__main__":
    main()