multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
414b4fe verified
Raw
History Blame Contribute Delete
5.96 kB
"""Portable PaDoc tree records and JSON/JSONL data loading."""
from __future__ import annotations
import json
from collections.abc import Iterator
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@dataclass
class Node:
prefix: str
children: list[Node] = field(default_factory=list)
@dataclass
class DataPiece:
query: str
response: Node
images: list[str] = field(default_factory=list)
@dataclass(frozen=True)
class TrainingRecord:
"""One raw record plus the directory used for relative image paths."""
value: dict[str, Any]
image_dir: Path
@dataclass(frozen=True)
class DataConfig:
records: list[TrainingRecord]
seed: int = 42
def parse_node(value: dict[str, Any]) -> Node:
if not isinstance(value, dict) or not isinstance(value.get("prefix"), str):
raise ValueError("Every response node requires a string 'prefix'.")
children = value.get("children", [])
if not isinstance(children, list):
raise ValueError("Node 'children' must be a list.")
return Node(prefix=value["prefix"], children=[parse_node(child) for child in children])
def parse_data_piece(value: dict[str, Any]) -> DataPiece:
if not isinstance(value, dict) or not isinstance(value.get("query"), str):
raise ValueError("Every training record requires a string 'query'.")
images = value.get("images", [])
if not isinstance(images, list) or not all(isinstance(item, str) for item in images):
raise ValueError("Record 'images' must be a list of paths.")
return DataPiece(
query=value["query"],
response=parse_node(value["response"]),
images=list(images),
)
def _iter_file(path: Path) -> Iterator[dict[str, Any]]:
if path.suffix.lower() == ".jsonl":
with path.open(encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
if not line.strip():
continue
try:
value = json.loads(line)
except json.JSONDecodeError as exc:
raise ValueError(f"{path}:{line_number}: invalid JSON: {exc}") from exc
if not isinstance(value, dict):
raise ValueError(f"{path}:{line_number}: expected a JSON object")
yield value
return
with path.open(encoding="utf-8") as handle:
values = json.load(handle)
if not isinstance(values, list):
raise ValueError(f"{path}: expected a top-level JSON array")
for index, value in enumerate(values):
if not isinstance(value, dict):
raise ValueError(f"{path}: record {index} is not an object")
yield value
def load_records(path: str | Path) -> list[dict[str, Any]]:
"""Load records from one JSON/JSONL file or a directory of such files."""
path = Path(path).expanduser().resolve()
if path.is_file():
return list(_iter_file(path))
if not path.is_dir():
raise FileNotFoundError(f"Data path does not exist: {path}")
files = sorted([*path.glob("*.json"), *path.glob("*.jsonl")])
if not files:
raise FileNotFoundError(f"No JSON or JSONL files found under {path}")
records: list[dict[str, Any]] = []
for file_path in files:
records.extend(_iter_file(file_path))
return records
def records_from_path(
data_path: str | Path,
*,
image_dir: str | Path = ".",
) -> list[TrainingRecord]:
root = Path(image_dir).expanduser().resolve()
return [TrainingRecord(value, root) for value in load_records(data_path)]
def load_data_config(path: str | Path) -> DataConfig:
"""Load a portable YAML mix whose paths are relative to the YAML file.
Schema::
seed: 42
sources:
- path: ../examples/train.jsonl
image_dir: ../examples/images
repeat: 1
"""
try:
import yaml
except ImportError as exc: # pragma: no cover - declared dependency
raise RuntimeError("PyYAML is required for --data-config") from exc
config_path = Path(path).expanduser().resolve()
with config_path.open(encoding="utf-8") as handle:
raw = yaml.safe_load(handle)
if not isinstance(raw, dict):
raise ValueError(f"{config_path}: top-level YAML must be a mapping")
unknown = set(raw) - {"seed", "sources"}
if unknown:
raise ValueError(f"{config_path}: unknown keys: {sorted(unknown)}")
sources = raw.get("sources")
if not isinstance(sources, list) or not sources:
raise ValueError(f"{config_path}: 'sources' must be a non-empty list")
records: list[TrainingRecord] = []
for index, source in enumerate(sources):
if not isinstance(source, dict):
raise ValueError(f"{config_path}: sources[{index}] must be a mapping")
source_unknown = set(source) - {"path", "image_dir", "repeat"}
if source_unknown:
raise ValueError(
f"{config_path}: sources[{index}] unknown keys: {sorted(source_unknown)}"
)
if not isinstance(source.get("path"), str):
raise ValueError(f"{config_path}: sources[{index}] requires string 'path'")
repeat = source.get("repeat", 1)
if not isinstance(repeat, int) or repeat < 1:
raise ValueError(f"{config_path}: sources[{index}].repeat must be >= 1")
source_path = (config_path.parent / source["path"]).resolve()
image_value = source.get("image_dir")
image_dir = (
(config_path.parent / image_value).resolve()
if isinstance(image_value, str)
else (source_path.parent if source_path.is_file() else source_path)
)
loaded = [TrainingRecord(value, image_dir) for value in load_records(source_path)]
for _ in range(repeat):
records.extend(loaded)
return DataConfig(records=records, seed=int(raw.get("seed", 42)))