Add facial diversity gate for character assets
This commit is contained in:
109
scripts/validate_assets.py
Normal file
109
scripts/validate_assets.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate generated character asset manifest and PNG files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DATA = ROOT / "data"
|
||||
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
print(f"ERROR: {message}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def png_dimensions(path: Path) -> tuple[int, int]:
|
||||
with path.open("rb") as handle:
|
||||
signature = handle.read(8)
|
||||
if signature != PNG_SIGNATURE:
|
||||
fail(f"{path} is not a PNG file")
|
||||
|
||||
length_bytes = handle.read(4)
|
||||
chunk_type = handle.read(4)
|
||||
if len(length_bytes) != 4 or chunk_type != b"IHDR":
|
||||
fail(f"{path} is missing a PNG IHDR chunk")
|
||||
|
||||
length = struct.unpack(">I", length_bytes)[0]
|
||||
if length < 8:
|
||||
fail(f"{path} has an invalid PNG IHDR chunk")
|
||||
|
||||
width, height = struct.unpack(">II", handle.read(8))
|
||||
return width, height
|
||||
|
||||
|
||||
def main() -> None:
|
||||
characters_doc = load_json(DATA / "characters.json")
|
||||
assets_doc = load_json(DATA / "character_assets.json")
|
||||
|
||||
character_ids = [character["id"] for character in characters_doc["characters"]]
|
||||
character_id_set = set(character_ids)
|
||||
asset_entries = assets_doc["assets"]
|
||||
asset_ids = [entry["character_id"] for entry in asset_entries]
|
||||
|
||||
if set(asset_ids) != character_id_set:
|
||||
missing = sorted(character_id_set.difference(asset_ids))
|
||||
extra = sorted(set(asset_ids).difference(character_id_set))
|
||||
fail(f"asset manifest mismatch; missing={missing}, extra={extra}")
|
||||
|
||||
if len(asset_ids) != len(set(asset_ids)):
|
||||
fail("asset manifest contains duplicate character ids")
|
||||
|
||||
minimum = assets_doc["minimum_dimensions"]
|
||||
min_width = minimum["width"]
|
||||
min_height = minimum["height"]
|
||||
accepted = 0
|
||||
|
||||
reference = ROOT / assets_doc["style"]["reference"]
|
||||
if not reference.exists():
|
||||
fail(f"style reference is missing: {reference}")
|
||||
png_dimensions(reference)
|
||||
|
||||
for entry in asset_entries:
|
||||
status = entry["status"]
|
||||
if status not in {"accepted", "needs_revision", "pending", "rejected"}:
|
||||
fail(f"{entry['character_id']} has invalid status {status}")
|
||||
|
||||
if status not in {"accepted", "needs_revision"}:
|
||||
continue
|
||||
|
||||
if status == "accepted":
|
||||
accepted += 1
|
||||
|
||||
if "path" not in entry:
|
||||
fail(f"{entry['character_id']} has status {status} but no path")
|
||||
|
||||
path = ROOT / entry["path"]
|
||||
if not path.exists():
|
||||
fail(f"{entry['character_id']} asset is missing: {path}")
|
||||
|
||||
width, height = png_dimensions(path)
|
||||
if width < min_width or height < min_height:
|
||||
fail(
|
||||
f"{entry['character_id']} asset is too small: "
|
||||
f"{width}x{height}, minimum {min_width}x{min_height}"
|
||||
)
|
||||
|
||||
revision_count = sum(
|
||||
1 for entry in asset_entries if entry["status"] == "needs_revision"
|
||||
)
|
||||
print(
|
||||
"Validated character asset manifest: "
|
||||
f"{accepted}/{len(asset_entries)} accepted, "
|
||||
f"{revision_count} needs revision."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user