tiny-turn-detector / scripts /inspect_dataset.py
Yash-V1002's picture
Deploy Tiny Turn Detector
875e4af verified
Raw
History Blame Contribute Delete
8.07 kB
"""
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()