Files
aetherbound-guild/tools/verify_pty001_clean_master.py
T

113 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Verify and document the PTY-001 clean static coordinate master."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_APPROVED = ROOT / "docs/design-authority/pages/PTY-001-party-line/approved/v001/master.png"
DEFAULT_REGIONS = ROOT / "docs/design-authority/pages/PTY-001-party-line/reconstruction/v001/static-master/dynamic-regions.json"
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def rect_crop(array: np.ndarray, bounds: list[int]) -> np.ndarray:
left, top, right, bottom = bounds
return array[top:bottom, left:right]
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--candidate", required=True, type=Path)
parser.add_argument("--approved", type=Path, default=DEFAULT_APPROVED)
parser.add_argument("--regions", type=Path, default=DEFAULT_REGIONS)
parser.add_argument("--report", required=True, type=Path)
parser.add_argument("--preview", type=Path)
parser.add_argument("--max-static-mae", type=float, default=24.0)
args = parser.parse_args()
authority = json.loads(args.regions.read_text(encoding="utf-8"))
expected_size = tuple(authority["design_size"])
approved_image = Image.open(args.approved).convert("RGB")
candidate_image = Image.open(args.candidate).convert("RGB")
if approved_image.size != expected_size:
raise SystemExit(f"approved size {approved_image.size} != {expected_size}")
if candidate_image.size != expected_size:
raise SystemExit(f"candidate size {candidate_image.size} != {expected_size}")
approved = np.asarray(approved_image, dtype=np.float32)
candidate = np.asarray(candidate_image, dtype=np.float32)
protected = []
for region in authority["static_protected_regions"]:
source = rect_crop(approved, region["bounds"])
edited = rect_crop(candidate, region["bounds"])
mae = float(np.abs(source - edited).mean())
protected.append({"id": region["id"], "bounds": region["bounds"], "mae": round(mae, 4)})
dynamic = []
for region in authority["regions"]:
source = rect_crop(approved, region["bounds"])
edited = rect_crop(candidate, region["bounds"])
change_mae = float(np.abs(source - edited).mean())
dynamic.append({"id": region["id"], "bounds": region["bounds"], "change_mae": round(change_mae, 4)})
max_static = max(item["mae"] for item in protected)
changed_dynamic = sum(item["change_mae"] >= 4.0 for item in dynamic)
ok = max_static <= args.max_static_mae and changed_dynamic == len(dynamic)
report = {
"schema_version": 1,
"page_id": "PTY-001",
"candidate": args.candidate.relative_to(ROOT).as_posix() if args.candidate.is_relative_to(ROOT) else str(args.candidate),
"approved": args.approved.relative_to(ROOT).as_posix() if args.approved.is_relative_to(ROOT) else str(args.approved),
"candidate_sha256": sha256(args.candidate),
"approved_sha256": sha256(args.approved),
"dimensions": list(candidate_image.size),
"mode": Image.open(args.candidate).mode,
"protected_static_regions": protected,
"dynamic_regions": dynamic,
"thresholds": {"max_static_mae": args.max_static_mae, "min_dynamic_change_mae": 4.0},
"summary": {
"ok": ok,
"max_static_mae": round(max_static, 4),
"changed_dynamic_regions": changed_dynamic,
"dynamic_region_count": len(dynamic),
},
"manual_review_required": [
"no characters, cord, position values, relation placeholders or selected-state effects remain",
"painted ground and walls are coherent behind removed content",
"HUD frames and fixed icons retain the approved line weight and geometry",
"all writable plates and parchment interiors contain no text or pseudo-writing",
],
}
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
if args.preview:
preview = candidate_image.copy()
draw = ImageDraw.Draw(preview, "RGBA")
for region in authority["regions"]:
draw.rectangle(region["bounds"], outline=(225, 79, 62, 230), width=4)
for region in authority["static_protected_regions"]:
draw.rectangle(region["bounds"], outline=(52, 172, 126, 220), width=3)
args.preview.parent.mkdir(parents=True, exist_ok=True)
preview.save(args.preview, optimize=True)
marker = "ABG_PTY001_CLEAN_MASTER_OK" if ok else "ABG_PTY001_CLEAN_MASTER_FAILED"
print(f"{marker} static_max_mae={max_static:.4f} dynamic={changed_dynamic}/{len(dynamic)} report={args.report}")
if not ok:
raise SystemExit(1)
if __name__ == "__main__":
main()