Spaces:
Running on Zero
Running on Zero
| """ | |
| Split strategy: since there are no speaker/session IDs (confirmed — | |
| docs/INITIAL_ANALYSIS.md §4), this module implements two comparable split | |
| strategies over the DEVELOPMENT subset only (never the official held-out | |
| test set, which is used as-is for final reporting): | |
| - split_random: plain random split, ignoring `dataset` (source) grouping. | |
| - split_source_aware: group-disjoint split by the `dataset` column, so no | |
| single source/voice-engine appears in both train and validation. | |
| The Phase 2 brief explicitly asks us to compare these two, to quantify | |
| whether source-specific acoustic characteristics make random evaluation | |
| overly optimistic. This module produces the splits; experiments/EXPERIMENTS.md | |
| records the actual comparison once real data is available. | |
| IMPORTANT: this is a source-DISJOINT split, not a speaker-disjoint split. | |
| The two are not equivalent, and this module's naming/docstrings are | |
| deliberately explicit about that so no downstream document accidentally | |
| implies otherwise. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| def split_random( | |
| records: list[dict], | |
| val_frac: float = 0.2, | |
| seed: int = 42, | |
| ) -> tuple[list[dict], list[dict]]: | |
| rng = np.random.default_rng(seed) | |
| idx = np.arange(len(records)) | |
| rng.shuffle(idx) | |
| n_val = int(round(len(records) * val_frac)) | |
| val_idx = set(idx[:n_val].tolist()) | |
| train, val = [], [] | |
| for i, r in enumerate(records): | |
| (val if i in val_idx else train).append(r) | |
| return train, val | |
| def split_source_aware( | |
| records: list[dict], | |
| val_frac: float = 0.2, | |
| seed: int = 42, | |
| source_key: str = "dataset", | |
| ) -> tuple[list[dict], list[dict]]: | |
| """Group-disjoint split by `source_key` (the `dataset` / voice-engine | |
| column). Whole sources are assigned entirely to train or entirely to | |
| val, chosen to get val_frac of the *records* (not sources) into val, | |
| via a greedy bin-packing over source sizes (deterministic given seed | |
| for tie-breaking order). | |
| """ | |
| rng = np.random.default_rng(seed) | |
| by_source: dict[str, list[dict]] = {} | |
| for r in records: | |
| by_source.setdefault(str(r.get(source_key)), []).append(r) | |
| sources = list(by_source.keys()) | |
| rng.shuffle(sources) # randomize assignment order for tie-breaking | |
| target_val_n = int(round(len(records) * val_frac)) | |
| val, train = [], [] | |
| val_n = 0 | |
| for s in sources: | |
| group = by_source[s] | |
| if val_n < target_val_n: | |
| val.extend(group) | |
| val_n += len(group) | |
| else: | |
| train.extend(group) | |
| return train, val | |
| def source_overlap(train: list[dict], val: list[dict], source_key: str = "dataset") -> set: | |
| """Returns the set of source values present in BOTH train and val. | |
| Should be empty for split_source_aware; will typically be non-empty for | |
| split_random (that's the point of the comparison). | |
| """ | |
| train_sources = {str(r.get(source_key)) for r in train} | |
| val_sources = {str(r.get(source_key)) for r in val} | |
| return train_sources & val_sources | |