Spaces:
Running on Zero
Running on Zero
File size: 5,963 Bytes
414b4fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | """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)))
|