Spaces:
Running on Zero
Running on Zero
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()
|