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