File size: 6,127 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
"""
Create a reproducible, stratified development subset of
pipecat-ai/smart-turn-data-v3.2-train, WITHOUT downloading/materializing
the full 41GB dataset.

Status: this script is complete and ready to run, but has NOT been executed
in this sandbox — `datasets`/`huggingface_hub` aren't installed here and
`bash` has no route to huggingface.co (confirmed 403 host_not_allowed via
curl; see docs/INITIAL_ANALYSIS.md and experiments/EXPERIMENTS.md). Run this
in an environment with network access and `pip install datasets`.

Usage:
    python scripts/create_dev_subset.py \
        --target-n 10000 \
        --seed 42 \
        --max-scan 200000 \
        --out data/processed/dev_subset_manifest.jsonl

Methodology (also written into the output manifest's header for
reproducibility):
  1. Stream the dataset (`streaming=True`) so we never download the full
     41GB just to build a 10k-example subset.
  2. Compute per-clip `duration_sec` and `duration_bucket` from the audio
     array's length/sample rate as each streamed record is visited (cheap —
     doesn't require decoding beyond what `datasets` already does to expose
     the array).
  3. Apply stratified reservoir sampling (src/turn_detector/data.py) across
     endpoint_bool x language x dataset(source) x synthetic x midfiller x
     endfiller x duration_bucket, using a two-pass approach over the first
     `--max-scan` streamed records (pass 1: count stratum sizes; pass 2:
     per-stratum reservoir sampling). This means `--max-scan` records are
     scanned twice; the dataset itself is never materialized beyond that.
  4. The chosen `target_n` records' `id` values (not the audio itself) are
     written to the manifest, plus the exact sampling parameters, so the
     subset is exactly reproducible from the manifest + original dataset.
  5. The test set (pipecat-ai/smart-turn-data-v3.2-test) is never touched
     by this script — it exists only to build a TRAIN-side dev subset.
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))

from turn_detector.data import (
    TRAIN_DATASET_ID, load_hf_dataset, stratified_reservoir_sample,
    duration_bucket, DevSubsetManifest, DatasetAccessError,
)


def record_to_stratum_record(record: dict) -> dict:
    """Extract only the lightweight metadata fields needed for
    stratification + the id needed to re-fetch the audio later — never
    keeps the decoded audio array in memory longer than needed to compute
    duration.
    """
    audio = record.get("audio")
    duration_sec = None
    if isinstance(audio, dict) and "array" in audio and "sampling_rate" in audio:
        duration_sec = len(audio["array"]) / audio["sampling_rate"]

    return {
        "id": record.get("id"),
        "endpoint_bool": record.get("endpoint_bool"),
        "language": record.get("language"),
        "dataset": record.get("dataset"),
        "synthetic": record.get("synthetic"),
        "midfiller": record.get("midfiller"),
        "endfiller": record.get("endfiller"),
        "duration_sec": duration_sec,
    }


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--target-n", type=int, default=10_000)
    ap.add_argument("--seed", type=int, default=42)
    ap.add_argument(
        "--max-scan", type=int, default=None,
        help="Cap on records scanned per pass for stratum-size counting "
        "and reservoir filling. Default: scan the whole stream (safest for "
        "correctness, slowest for a 270k-row dataset). Set e.g. 200000 to "
        "bound runtime at some accuracy cost in stratum proportion "
        "accuracy for very rare strata.",
    )
    ap.add_argument("--out", type=Path, default=Path("data/processed/dev_subset_manifest.jsonl"))
    args = ap.parse_args()

    try:
        ds = load_hf_dataset(TRAIN_DATASET_ID, split="train", streaming=True)
    except DatasetAccessError as e:
        print(f"ERROR: could not load dataset: {e}", file=sys.stderr)
        print(
            "This script cannot run without network access to Hugging Face. "
            "See docs/INITIAL_ANALYSIS.md for the current sandbox limitation.",
            file=sys.stderr,
        )
        sys.exit(1)

    # Two lightweight passes over the (streamed) dataset. Streaming
    # datasets support repeated iteration by re-creating the iterator; we
    # rely on `stratified_reservoir_sample`'s two-`for record in records`
    # loops, so we materialize just the lightweight metadata (not audio)
    # into a list first — this is a deliberate, documented tradeoff: it
    # means `--max-scan` records of *metadata only* are held in memory
    # (small), while full audio arrays are never retained beyond a single
    # record's processing.
    print(f"Scanning up to {args.max_scan or 'ALL'} records for stratification metadata...")
    stratum_records = []
    for i, record in enumerate(ds):
        stratum_records.append(record_to_stratum_record(record))
        if args.max_scan is not None and i + 1 >= args.max_scan:
            break
        if (i + 1) % 10_000 == 0:
            print(f"  scanned {i + 1} records...")

    print(f"Scanned {len(stratum_records)} records. Running stratified sampling...")
    sample = stratified_reservoir_sample(
        stratum_records, target_n=args.target_n, seed=args.seed, max_scan=None,
    )

    manifest = DevSubsetManifest(
        source_dataset_id=TRAIN_DATASET_ID,
        source_split="train",
        target_n=args.target_n,
        actual_n=len(sample),
        seed=args.seed,
        stratify_columns=("endpoint_bool", "language", "dataset", "synthetic", "midfiller", "endfiller", "duration_bucket"),
        max_scan=args.max_scan,
    )

    args.out.parent.mkdir(parents=True, exist_ok=True)
    with open(args.out, "w") as f:
        f.write(json.dumps({"_manifest": manifest.to_dict()}) + "\n")
        for r in sample:
            f.write(json.dumps(r) + "\n")

    print(f"Wrote {len(sample)} records + manifest header to {args.out}")


if __name__ == "__main__":
    main()