Spaces:
Running
Running
File size: 6,481 Bytes
2e175db | 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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | """
Combine per-source manifests into a single master manifest.
The master manifest is the authoritative legal record for the dataset. It is:
• Committed alongside the trained model (so the lineage is auditable).
• Used by the training script (no image is loaded that isn't in the manifest).
• The first thing a lawyer or auditor will ask to see.
Stage 3A manifest schema
------------------------
Required legal/training fields:
path, class, source, license, license_url, sha256
Optional generator metadata fields:
generator, model_family, model_id, prompt, seed, width, height,
generation_params_json
Missing optional values are written as empty strings. This keeps Stage 2
manifest fragments readable while giving Stage 3A scripts stable columns for
per-generator evaluation.
Usage
-----
python scripts/dataset/build_manifest.py \
--inputs data/raw/real_manifest.csv data/raw/ai_generated_manifest.csv \
--out data/manifest.csv
python scripts/dataset/build_manifest.py \
--input-dir data/raw \
--out data/manifest.csv
"""
from __future__ import annotations
import argparse
import csv
from pathlib import Path
from generation_utils import APPROVED_GENERATORS
REQUIRED_FIELDS = ["path", "class", "source", "license", "license_url", "sha256"]
OPTIONAL_STAGE3A_FIELDS = [
"generator",
"model_family",
"model_id",
"prompt",
"seed",
"width",
"height",
"generation_params_json",
]
MANIFEST_FIELDS = REQUIRED_FIELDS + OPTIONAL_STAGE3A_FIELDS
MANIFEST_FRAGMENT_GLOB = "*_manifest.csv"
GENERATOR_SPECS_BY_SOURCE = {
spec.source: spec for spec in APPROVED_GENERATORS.values()
}
def _normalise_row(row: dict[str, str]) -> dict[str, str]:
"""Return a manifest row with all Stage 3A optional fields present."""
normalised = dict(row)
source = normalised.get("source", "")
spec = GENERATOR_SPECS_BY_SOURCE.get(source)
if normalised.get("class") == "ai_generated" and spec is not None:
if not normalised.get("generator"):
normalised["generator"] = spec.generator
if not normalised.get("model_family"):
normalised["model_family"] = spec.model_family
if not normalised.get("model_id"):
normalised["model_id"] = spec.model_id
for field in OPTIONAL_STAGE3A_FIELDS:
if normalised.get(field) is None:
normalised[field] = ""
else:
normalised[field] = str(normalised.get(field, ""))
return normalised
def _discover_manifest_fragments(input_dirs: list[Path], out: Path) -> list[Path]:
"""Find manifest fragments below each input directory in stable order."""
discovered: list[Path] = []
out_resolved = out.resolve()
for input_dir in input_dirs:
if not input_dir.exists():
raise FileNotFoundError(f"Input directory does not exist: {input_dir}")
if not input_dir.is_dir():
raise NotADirectoryError(f"Input path is not a directory: {input_dir}")
for path in sorted(input_dir.rglob(MANIFEST_FRAGMENT_GLOB)):
if path.resolve() == out_resolved:
continue
discovered.append(path)
return discovered
def _unique_paths(paths: list[Path]) -> list[Path]:
"""Deduplicate paths while preserving caller/discovery order."""
unique: list[Path] = []
seen: set[Path] = set()
for path in paths:
resolved = path.resolve()
if resolved in seen:
continue
seen.add(resolved)
unique.append(path)
return unique
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--inputs",
type=Path,
nargs="+",
default=[],
help="Explicit manifest fragments to merge",
)
parser.add_argument(
"--input-dir",
type=Path,
action="append",
default=[],
help=(
f"Directory to scan recursively for {MANIFEST_FRAGMENT_GLOB}; "
"can be passed multiple times"
),
)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
input_paths = _unique_paths(
[*args.inputs, *_discover_manifest_fragments(args.input_dir, args.out)]
)
if not input_paths:
raise ValueError("Provide at least one --inputs file or --input-dir")
rows: list[dict] = []
for src in input_paths:
if not src.exists():
raise FileNotFoundError(f"Input manifest does not exist: {src}")
with src.open() as fh:
reader = csv.DictReader(fh)
for r in reader:
missing = [f for f in REQUIRED_FIELDS if not r.get(f)]
if missing:
raise ValueError(
f"{src}: row missing required fields {missing}: {r}"
)
rows.append(_normalise_row(r))
# Detect duplicates by sha256 — important for license cleanliness AND
# to avoid train/test leakage.
seen: dict[str, str] = {}
deduped: list[dict] = []
for r in rows:
sha = r["sha256"]
if sha in seen:
print(f" dropping duplicate {r['path']} (matches {seen[sha]})")
continue
seen[sha] = r["path"]
deduped.append(r)
args.out.parent.mkdir(parents=True, exist_ok=True)
# Write a stable schema first, then any extra legacy/source-specific fields.
fieldnames = sorted({k for r in deduped for k in r.keys()})
ordered = MANIFEST_FIELDS + [f for f in fieldnames if f not in MANIFEST_FIELDS]
with args.out.open("w", newline="") as fh:
writer = csv.DictWriter(fh, fieldnames=ordered)
writer.writeheader()
writer.writerows(deduped)
classes: dict[str, int] = {}
for r in deduped:
classes[r["class"]] = classes.get(r["class"], 0) + 1
print(f"\nMaster manifest: {args.out}")
print(f" fragments: {len(input_paths)}")
print(f" total rows: {len(deduped)}")
for cls, n in sorted(classes.items()):
print(f" {cls}: {n}")
generators: dict[str, int] = {}
for r in deduped:
if r["class"] != "ai_generated":
continue
generator = r.get("generator") or r["source"]
generators[generator] = generators.get(generator, 0) + 1
for generator, n in sorted(generators.items()):
print(f" generator {generator}: {n}")
if __name__ == "__main__":
main()
|