Spaces:
Running on Zero
Running on Zero
| """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 | |
| class Node: | |
| prefix: str | |
| children: list[Node] = field(default_factory=list) | |
| class DataPiece: | |
| query: str | |
| response: Node | |
| images: list[str] = field(default_factory=list) | |
| class TrainingRecord: | |
| """One raw record plus the directory used for relative image paths.""" | |
| value: dict[str, Any] | |
| image_dir: Path | |
| 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))) | |