suvradeepp's picture
Publish Tiny Hinglish Turn Detector development preview
35d483e verified
Raw
History Blame Contribute Delete
9.48 kB
"""Lazy local-Parquet and Hugging Face dataset ingestion."""
from __future__ import annotations
import csv
import gzip
import json
from pathlib import Path
from typing import Any, Iterable, Iterator, Mapping, Sequence
class OptionalDependencyError(ImportError):
"""An optional data backend is required for the requested source."""
class DatasetReadError(RuntimeError):
"""A source exists but could not be decoded into records."""
def _open_text(path: Path):
if path.suffix == ".gz":
return gzip.open(path, "rt", encoding="utf-8", newline="")
return path.open("r", encoding="utf-8", newline="")
def _with_provenance(row: Mapping[str, Any], source: str, index: int) -> dict[str, Any]:
result = dict(row)
result.setdefault("__source_file", source)
result.setdefault("__source_row", index)
return result
def _iter_jsonl(path: Path) -> Iterator[dict[str, Any]]:
with _open_text(path) as handle:
for index, line in enumerate(handle):
if not line.strip():
continue
try:
row = json.loads(line)
except json.JSONDecodeError as exc:
raise DatasetReadError(f"invalid JSON at {path}:{index + 1}: {exc}") from exc
if not isinstance(row, Mapping):
raise DatasetReadError(f"expected an object at {path}:{index + 1}")
yield _with_provenance(row, str(path), index)
def _iter_json(path: Path) -> Iterator[dict[str, Any]]:
try:
with _open_text(path) as handle:
payload = json.load(handle)
except (OSError, json.JSONDecodeError) as exc:
raise DatasetReadError(f"cannot read JSON source {path}: {exc}") from exc
if isinstance(payload, Mapping):
for key in ("records", "data", "examples", "rows"):
if key in payload:
payload = payload[key]
break
if not isinstance(payload, list):
raise DatasetReadError(f"JSON source {path} must contain a list of records")
for index, row in enumerate(payload):
if not isinstance(row, Mapping):
raise DatasetReadError(f"expected an object at {path} record {index}")
yield _with_provenance(row, str(path), index)
def _iter_csv(path: Path) -> Iterator[dict[str, Any]]:
with _open_text(path) as handle:
reader = csv.DictReader(handle)
for index, row in enumerate(reader):
yield _with_provenance(row, str(path), index)
def _import_pyarrow_parquet():
try:
import pyarrow.parquet as parquet # type: ignore[import-not-found]
except ImportError as exc:
raise OptionalDependencyError(
"reading local Parquet requires pyarrow; install the project's data dependencies"
) from exc
return parquet
def iter_parquet_records(
paths: Sequence[str | Path],
*,
batch_size: int = 256,
columns: Sequence[str] | None = None,
) -> Iterator[dict[str, Any]]:
"""Stream rows from Parquet files without decoding Hugging Face audio."""
if batch_size <= 0:
raise ValueError("batch_size must be positive")
parquet = _import_pyarrow_parquet()
for raw_path in paths:
path = Path(raw_path)
try:
parquet_file = parquet.ParquetFile(str(path))
row_index = 0
for batch in parquet_file.iter_batches(batch_size=batch_size, columns=columns):
for row in batch.to_pylist():
if not isinstance(row, Mapping):
raise DatasetReadError(f"non-object Parquet row in {path}")
yield _with_provenance(row, str(path), row_index)
row_index += 1
except OptionalDependencyError:
raise
except Exception as exc:
raise DatasetReadError(f"cannot read Parquet source {path}: {exc}") from exc
def _local_files(source: Path) -> list[Path]:
if source.is_file():
return [source]
if not source.exists():
raise FileNotFoundError(f"dataset source does not exist: {source}")
if not source.is_dir():
raise DatasetReadError(f"dataset source is not a file or directory: {source}")
supported = {".parquet", ".jsonl", ".ndjson", ".json", ".csv"}
files = [path for path in source.rglob("*") if path.is_file() and path.suffix.lower() in supported]
files.extend(
path
for path in source.rglob("*.jsonl.gz")
if path.is_file() and path not in files
)
return sorted(files, key=lambda item: item.as_posix())
def iter_local_records(
source: str | Path | Sequence[str | Path],
*,
batch_size: int = 256,
columns: Sequence[str] | None = None,
) -> Iterator[dict[str, Any]]:
"""Iterate records from local Parquet, JSON(L), or CSV sources."""
roots = [source] if isinstance(source, (str, Path)) else list(source)
files: list[Path] = []
for root in roots:
files.extend(_local_files(Path(root)))
files = sorted(set(files), key=lambda item: item.as_posix())
if not files:
raise DatasetReadError("no supported dataset files were found")
parquet_paths = [path for path in files if path.suffix.lower() == ".parquet"]
other_paths = [path for path in files if path.suffix.lower() != ".parquet"]
if parquet_paths:
yield from iter_parquet_records(parquet_paths, batch_size=batch_size, columns=columns)
for path in other_paths:
name = path.name.lower()
if name.endswith((".jsonl", ".ndjson", ".jsonl.gz")):
yield from _iter_jsonl(path)
elif name.endswith(".json"):
yield from _iter_json(path)
elif name.endswith(".csv"):
yield from _iter_csv(path)
def _normalize_hf_id(source: str) -> str:
value = source
if value.startswith("hf://datasets/"):
value = value[len("hf://datasets/") :]
elif value.startswith("hf://"):
value = value[len("hf://") :]
return value.strip("/")
def iter_hf_records(
dataset_id: str,
*,
split: str = "train",
revision: str | None = None,
token: str | None = None,
streaming: bool = True,
) -> Iterator[dict[str, Any]]:
"""Stream raw rows from a Hugging Face dataset with audio decoding disabled."""
try:
import datasets # type: ignore[import-not-found]
except ImportError as exc:
raise OptionalDependencyError(
"reading a Hugging Face dataset requires the 'datasets' package"
) from exc
dataset_id = _normalize_hf_id(dataset_id)
kwargs: dict[str, Any] = {"split": split, "streaming": streaming}
if revision is not None:
kwargs["revision"] = revision
if token is not None:
kwargs["token"] = token
try:
dataset = datasets.load_dataset(dataset_id, **kwargs)
except TypeError as exc:
# Compatibility with older datasets releases which used use_auth_token.
if token is None or "token" not in str(exc):
raise DatasetReadError(f"cannot load Hugging Face dataset {dataset_id}: {exc}") from exc
kwargs.pop("token", None)
kwargs["use_auth_token"] = token
try:
dataset = datasets.load_dataset(dataset_id, **kwargs)
except Exception as retry_exc:
raise DatasetReadError(f"cannot load Hugging Face dataset {dataset_id}: {retry_exc}") from retry_exc
except Exception as exc:
raise DatasetReadError(f"cannot load Hugging Face dataset {dataset_id}: {exc}") from exc
# ``decode=False`` keeps encoded bytes available and avoids torchcodec/ffmpeg.
try:
if "audio" in dataset.features:
dataset = dataset.cast_column("audio", datasets.Audio(decode=False))
except (AttributeError, TypeError, ValueError):
# Some custom builders already return undecoded mappings or do not expose
# cast_column in streaming mode. Defer representation validation to audio.py.
pass
provenance = f"hf://datasets/{dataset_id}@{revision or 'default'}:{split}"
for index, row in enumerate(dataset):
if not isinstance(row, Mapping):
raise DatasetReadError(f"Hugging Face dataset {dataset_id} yielded a non-object row")
yield _with_provenance(row, provenance, index)
def _looks_like_hf_id(source: str) -> bool:
if source.startswith("hf://"):
return True
return source.count("/") == 1 and not source.startswith(("./", "../", "/"))
def iter_records(
source: str | Path | Sequence[str | Path],
*,
split: str = "train",
revision: str | None = None,
token: str | None = None,
streaming: bool = True,
batch_size: int = 256,
columns: Sequence[str] | None = None,
limit: int | None = None,
) -> Iterator[dict[str, Any]]:
"""Iterate local or Hugging Face rows through one stable entry point."""
if limit is not None and limit < 0:
raise ValueError("limit cannot be negative")
if isinstance(source, str) and _looks_like_hf_id(source) and not Path(source).exists():
iterator: Iterable[dict[str, Any]] = iter_hf_records(
source,
split=split,
revision=revision,
token=token,
streaming=streaming,
)
else:
iterator = iter_local_records(source, batch_size=batch_size, columns=columns)
for index, row in enumerate(iterator):
if limit is not None and index >= limit:
break
yield row