vocal-render / src /vocalrender /training /svs_loading.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
5ed07ee verified
Raw
History Blame Contribute Delete
6.76 kB
"""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