Spaces:
Running on Zero
Running on Zero
| """Arrow dataset loading and train/validation splitting for SVS training.""" | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| from typing import Dict, List, Optional, Sequence, Tuple | |
| from datasets import Dataset, concatenate_datasets, load_from_disk | |
| def _resolve_shard_paths(preprocessed_dir: Path) -> Optional[List[str]]: | |
| """Return the shard files in ``load_from_disk`` order, or ``None``. | |
| ``save_to_disk`` records the shard list (and their order) in | |
| ``state.json``'s ``_data_files``. Loading those shards individually and | |
| concatenating them reproduces *exactly* the row order ``load_from_disk`` | |
| yields — but only when there is no ``_indices_data_files`` remap (i.e. the | |
| dataset wasn't saved after a ``.select``/``.shuffle``). When either the | |
| state file is missing or an indices remap is present, return ``None`` so | |
| callers fall back to the canonical (slow) ``load_from_disk``. | |
| """ | |
| state_path = preprocessed_dir / "state.json" | |
| if not state_path.exists(): | |
| return None | |
| with state_path.open() as f: | |
| state = json.load(f) | |
| if state.get("_indices_data_files"): | |
| return None | |
| data_files = state.get("_data_files") | |
| if not data_files: | |
| return None | |
| return [str(preprocessed_dir / entry["filename"]) for entry in data_files] | |
| def fast_load_from_disk(preprocessed_dir: str) -> Dataset: | |
| """Drop-in for ``load_from_disk`` that skips its full-corpus overhead. | |
| On the 71-shard / 229 GB SVS corpus, ``datasets.load_from_disk`` takes | |
| ~385 s and ~24 GB RSS before returning (NFS-pathological concat/validate of | |
| every shard). Concatenating the shards via per-shard ``Dataset.from_file`` | |
| mmaps yields the identical (lazy) dataset in ~20 s and ~1 GB. Row order is | |
| preserved (``state.json`` ``_data_files`` order), so any global row index | |
| stays consistent with a later ``load_from_disk`` read. Falls back to ``load_from_disk`` when the shard | |
| layout can't be resolved safely (see ``_resolve_shard_paths``). | |
| """ | |
| preprocessed_dir = Path(preprocessed_dir) | |
| shards = _resolve_shard_paths(preprocessed_dir) | |
| if not shards: | |
| return load_from_disk(str(preprocessed_dir)) | |
| if len(shards) == 1: | |
| return Dataset.from_file(shards[0]) | |
| return concatenate_datasets([Dataset.from_file(s) for s in shards]) | |
| def scan_dataset_metadata( | |
| preprocessed_dir: str, | |
| columns: Sequence[str] = ("dataset_name", "song_name"), | |
| ) -> Dict[str, list]: | |
| """Read only small metadata ``columns``, per-shard, for fast row filtering. | |
| Reading e.g. ``dataset_name`` this way costs ~22 s / ~1.2 GB on the full | |
| 3.5 M-row corpus — versus ~385 s / ~24 GB to ``load_from_disk`` first. Use | |
| it to decide *which* rows a script wants (which datasets / songs) before | |
| paying to materialize the heavy ``audio_feats`` blob via ``.select`` on a | |
| ``fast_load_from_disk`` view. Returned lists are aligned to the global row | |
| index (``state.json`` order), matching ``fast_load_from_disk`` / | |
| ``load_from_disk``. | |
| """ | |
| preprocessed_dir = Path(preprocessed_dir) | |
| columns = list(columns) | |
| shards = _resolve_shard_paths(preprocessed_dir) | |
| if not shards: | |
| ds = load_from_disk(str(preprocessed_dir)) | |
| return {c: ds[c] for c in columns} | |
| out: Dict[str, list] = {c: [] for c in columns} | |
| for shard in shards: | |
| ds = Dataset.from_file(shard) | |
| for c in columns: | |
| out[c].extend(ds[c]) | |
| del ds | |
| return out | |
| def load_preprocessed_svs_datasets( | |
| preprocessed_dir: str, | |
| val_datasets: Optional[List[str]] = None, | |
| val_songs: Optional[List[str]] = None, | |
| val_samples: int = 0, | |
| ) -> Tuple[Dataset, Optional[Dataset]]: | |
| """Load preprocessed SVS datasets from Arrow format and split into train/val.""" | |
| preprocessed_dir = Path(preprocessed_dir) | |
| if not preprocessed_dir.exists(): | |
| raise ValueError(f"Preprocessed data not found at {preprocessed_dir}") | |
| val_datasets = val_datasets or [] | |
| val_songs = val_songs or [] | |
| if val_datasets or val_songs: | |
| val_datasets_set = set(val_datasets) | |
| val_songs_set = set(val_songs) | |
| # fast_load_from_disk is a lazy per-shard concat (~27 s vs ~385 s for | |
| # load_from_disk); reading the two small label columns off it is another | |
| # ~4 s and never faults the heavy audio_feats blob. ``.select`` below is | |
| # lazy, so only the rows actually used materialize. | |
| all_ds = fast_load_from_disk(str(preprocessed_dir)) | |
| all_dataset_names = all_ds["dataset_name"] | |
| all_song_names = all_ds["song_name"] | |
| val_indices = [] | |
| train_indices = [] | |
| for idx, (dataset_name, song_name) in enumerate(zip(all_dataset_names, all_song_names)): | |
| if dataset_name in val_datasets_set or song_name in val_songs_set: | |
| val_indices.append(idx) | |
| else: | |
| train_indices.append(idx) | |
| train_ds = all_ds.select(train_indices) if train_indices else all_ds.select([]) | |
| val_ds = all_ds.select(val_indices) if val_indices else None | |
| if val_datasets: | |
| found = {all_dataset_names[idx] for idx in val_indices} & val_datasets_set | |
| missing = val_datasets_set - found | |
| if missing: | |
| print(f"Warning: val datasets not found: {missing}") | |
| counts = {} | |
| for idx in val_indices: | |
| dataset_name = all_dataset_names[idx] | |
| if dataset_name in val_datasets_set: | |
| counts[dataset_name] = counts.get(dataset_name, 0) + 1 | |
| print(f"Validation datasets: {counts}") | |
| if val_songs: | |
| found = {all_song_names[idx] for idx in val_indices} & val_songs_set | |
| missing = val_songs_set - found | |
| if missing: | |
| print(f"Warning: {len(missing)} val songs not found: {missing}") | |
| print(f"Found {len(found)} validation songs") | |
| print(f"Split: {len(train_indices)} train, {len(val_indices)} val") | |
| return train_ds, val_ds | |
| all_ds = fast_load_from_disk(str(preprocessed_dir)) | |
| if val_samples > 0: | |
| import random as rnd | |
| rnd_state = rnd.Random(42) | |
| indices = list(range(len(all_ds))) | |
| rnd_state.shuffle(indices) | |
| n_val = min(val_samples, len(all_ds) - 1) | |
| val_indices = indices[:n_val] | |
| train_indices = indices[n_val:] | |
| train_ds = all_ds.select(train_indices) | |
| val_ds = all_ds.select(val_indices) if val_indices else None | |
| print(f"Random split (seed=42): {len(train_indices)} train, {n_val} val") | |
| return train_ds, val_ds | |
| print(f"No validation split: {len(all_ds)} samples for training") | |
| return all_ds, None | |