Spaces:
Running on Zero
Running on Zero
| """ | |
| Dataset access layer for pipecat-ai/smart-turn-data-v3.2-{train,test}. | |
| Design constraints this module honors: | |
| - Must support HF `streaming=True` so exploratory work never requires | |
| downloading the full 41GB / 4.84GB parquet files (per Phase 2 brief). | |
| - Must not materialize the full dataset in memory. | |
| - Must be deterministic given a seed, for reproducible dev-subset creation. | |
| Status in THIS sandbox: this module has NOT been executed against the real | |
| dataset. `datasets` and `huggingface_hub` are not installed here, and pip | |
| cannot reach PyPI (confirmed: `pip install` fails with "No matching | |
| distribution found" — no route out), and `bash` network egress is | |
| proxy-blocked for huggingface.co specifically (confirmed via curl: HTTP 403, | |
| `x-deny-reason: host_not_allowed`). This is written to run correctly in an | |
| environment with real network access (e.g. the actual training environment | |
| this project will run in) and its logic (subset sampling, split logic) is | |
| unit-tested here against a small in-memory fake dataset that mimics the | |
| real schema — see tests/test_data.py. That is a legitimate way to validate | |
| sampling/splitting *logic* without the real data; it does NOT substitute | |
| for running this against the real dataset, and is never presented as such. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from typing import Iterable, Iterator, Optional | |
| import numpy as np | |
| TRAIN_DATASET_ID = "pipecat-ai/smart-turn-data-v3.2-train" | |
| TEST_DATASET_ID = "pipecat-ai/smart-turn-data-v3.2-test" | |
| # Confirmed schema (from the dataset's own README dataset_info YAML — | |
| # see docs/INITIAL_ANALYSIS.md §1). Listed here as a single source of truth | |
| # so downstream code fails loudly if the real schema doesn't match this | |
| # (rather than silently mis-indexing columns). | |
| EXPECTED_COLUMNS = { | |
| "audio", | |
| "id", | |
| "language", | |
| "endpoint_bool", | |
| "midfiller", | |
| "endfiller", | |
| "synthetic", | |
| "spoken_text", | |
| "dataset", | |
| } | |
| class DatasetAccessError(RuntimeError): | |
| pass | |
| def _require_datasets_lib(): | |
| try: | |
| import datasets # noqa: F401 | |
| return datasets | |
| except ImportError as e: | |
| raise DatasetAccessError( | |
| "The `datasets` library is not installed / could not reach the " | |
| "Hugging Face Hub in this environment. This function is written " | |
| "to work correctly wherever `datasets` and network access are " | |
| "available — install with `pip install datasets huggingface_hub` " | |
| "in such an environment." | |
| ) from e | |
| def load_hf_dataset( | |
| dataset_id: str, | |
| split: str = "train", | |
| streaming: bool = True, | |
| ): | |
| """Thin wrapper around `datasets.load_dataset`, defaulting to streaming | |
| mode so exploratory work doesn't require downloading the full dataset. | |
| Not executable in this sandbox (see module docstring) — raises | |
| DatasetAccessError with a clear message rather than pretending to | |
| succeed. | |
| """ | |
| ds_lib = _require_datasets_lib() | |
| try: | |
| ds = ds_lib.load_dataset(dataset_id, split=split, streaming=streaming) | |
| except Exception as e: | |
| raise DatasetAccessError( | |
| f"Failed to load {dataset_id} (split={split}, streaming={streaming}): {e}" | |
| ) from e | |
| # Fail loudly, early, if the live schema doesn't match what we've | |
| # documented — protects every downstream assumption in this codebase. | |
| if not streaming: | |
| cols = set(ds.column_names) | |
| else: | |
| # Streaming datasets don't expose column_names without peeking at | |
| # one example. | |
| first = next(iter(ds)) | |
| cols = set(first.keys()) | |
| missing = EXPECTED_COLUMNS - cols | |
| if missing: | |
| raise DatasetAccessError( | |
| f"{dataset_id} is missing expected columns {missing}. " | |
| f"Schema may have changed since docs/INITIAL_ANALYSIS.md was written — " | |
| f"re-verify before trusting any downstream code." | |
| ) | |
| return ds | |
| # --------------------------------------------------------------------------- | |
| # Reproducible stratified dev-subset sampling | |
| # --------------------------------------------------------------------------- | |
| STRATIFY_COLUMNS = ("endpoint_bool", "language", "dataset", "synthetic", "midfiller", "endfiller") | |
| def duration_bucket(duration_sec: float) -> str: | |
| """Coarse duration bucketing used as an extra stratification axis. | |
| Bucket edges are round numbers chosen for interpretability, not fit to | |
| any observed distribution (we don't have the full duration distribution | |
| to fit to — see docs/INITIAL_ANALYSIS.md §3). | |
| """ | |
| if duration_sec < 1.0: | |
| return "<1s" | |
| if duration_sec < 2.0: | |
| return "1-2s" | |
| if duration_sec < 4.0: | |
| return "2-4s" | |
| if duration_sec < 8.0: | |
| return "4-8s" | |
| return ">=8s" | |
| def _stratum_key(record: dict) -> tuple: | |
| key = [] | |
| for col in STRATIFY_COLUMNS: | |
| val = record.get(col) | |
| # None (null) is itself a meaningful stratum value (e.g. filler | |
| # metadata unavailable for this source) — must NOT be coerced to | |
| # False, since null != false (per Phase 2 brief §11 warning). | |
| key.append(str(val)) | |
| if "duration_sec" in record: | |
| key.append(duration_bucket(record["duration_sec"])) | |
| return tuple(key) | |
| def stratified_reservoir_sample( | |
| records: Iterable[dict], | |
| target_n: int, | |
| seed: int = 42, | |
| max_scan: Optional[int] = None, | |
| ) -> list: | |
| """Streaming-compatible stratified sampling. | |
| Approach: proportional allocation via per-stratum reservoir sampling. | |
| Because we're consuming a (potentially streaming, single-pass) iterator | |
| and don't know strata sizes in advance, this uses the standard two-pass- | |
| free approach: maintain a per-stratum reservoir sized proportionally as | |
| strata are discovered, using Algorithm R per stratum. This is | |
| deterministic given `seed` and the iteration order of `records`. | |
| This does NOT require materializing the full dataset — it holds at most | |
| `target_n` records (plus a small amount of per-stratum bookkeeping) in | |
| memory at any time, satisfying the "no full dataset materialization" | |
| requirement. | |
| Note on the allocation strategy: true proportional stratified sampling | |
| with unknown-in-advance strata sizes, in a single streaming pass, is a | |
| real algorithmic constraint — not solvable exactly without either (a) a | |
| first pass to count strata sizes, or (b) an online algorithm that | |
| approximates proportional allocation as it goes. We do a light first | |
| pass over up to `max_scan` records (default: unbounded, i.e. a full | |
| pass) to compute exact per-stratum counts, then a second pass to do | |
| proportional reservoir sampling per stratum. This means the function | |
| consumes an iterable twice if it's re-iterable (e.g. a non-streaming HF | |
| dataset). For a truly single-pass streaming source, pass | |
| `max_scan=<some cap>` and accept that allocation is approximate for | |
| strata whose true size wasn't fully observed within the cap — this | |
| tradeoff is intentional and documented rather than hidden. | |
| """ | |
| rng = np.random.default_rng(seed) | |
| # Pass 1: count stratum sizes (bounded by max_scan if given) | |
| stratum_counts: dict[tuple, int] = {} | |
| n_scanned = 0 | |
| for record in records: | |
| stratum_counts[_stratum_key(record)] = stratum_counts.get(_stratum_key(record), 0) + 1 | |
| n_scanned += 1 | |
| if max_scan is not None and n_scanned >= max_scan: | |
| break | |
| if n_scanned == 0: | |
| return [] | |
| total = sum(stratum_counts.values()) | |
| # Proportional target size per stratum (at least 1 if the stratum has | |
| # any members and target_n leaves room) | |
| target_per_stratum = { | |
| k: max(1, round(target_n * c / total)) for k, c in stratum_counts.items() | |
| } | |
| # Pass 2: reservoir-sample within each stratum up to its target size. | |
| # Requires records to be re-iterable for this two-pass approach; for a | |
| # single-pass-only streaming source, use the max_scan single-pass mode | |
| # below instead (kept as a separate documented code path, not hidden). | |
| reservoirs: dict[tuple, list] = {k: [] for k in stratum_counts} | |
| seen_counts: dict[tuple, int] = {k: 0 for k in stratum_counts} | |
| for record in records: | |
| key = _stratum_key(record) | |
| if key not in reservoirs: | |
| continue # discovered after max_scan cutoff in pass 1; skip | |
| seen_counts[key] += 1 | |
| cap = target_per_stratum[key] | |
| res = reservoirs[key] | |
| if len(res) < cap: | |
| res.append(record) | |
| else: | |
| j = rng.integers(0, seen_counts[key]) | |
| if j < cap: | |
| res[j] = record | |
| n_scanned_2 = sum(seen_counts.values()) | |
| if max_scan is not None and n_scanned_2 >= max_scan: | |
| break | |
| sample = [rec for res in reservoirs.values() for rec in res] | |
| rng.shuffle(sample) | |
| return sample[:target_n] if len(sample) > target_n else sample | |
| class DevSubsetManifest: | |
| """Records exactly how a dev subset was created, for reproducibility | |
| and for the "document exactly how it was sampled" requirement. | |
| """ | |
| source_dataset_id: str | |
| source_split: str | |
| target_n: int | |
| actual_n: int | |
| seed: int | |
| stratify_columns: tuple | |
| max_scan: Optional[int] | |
| def to_dict(self) -> dict: | |
| return { | |
| "source_dataset_id": self.source_dataset_id, | |
| "source_split": self.source_split, | |
| "target_n": self.target_n, | |
| "actual_n": self.actual_n, | |
| "seed": self.seed, | |
| "stratify_columns": list(self.stratify_columns), | |
| "max_scan": self.max_scan, | |
| } | |