← Files JuicyLucy AdsARCHIVED FILE

skills/static-localization/scripts/verify_campaign.py

6.6 KB · Oct 4, 2026 · 12:34 UTC

↓ Download file

#!/usr/bin/env python3
"""Verify one localized static-ad campaign from a JSON manifest."""

from __future__ import annotations

import argparse
import hashlib
import json
from pathlib import Path

from PIL import Image


def digest(path: Path) -> str:
    hasher = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(1024 * 1024), b""):
            hasher.update(block)
    return hasher.hexdigest()


def verify(manifest: dict) -> dict:
    source_root = Path(manifest["source_root"])
    target_root = Path(manifest["target_root"])
    source_code = manifest["source_code"]
    target_code = manifest["target_code"]
    expected_suffix = manifest.get("expected_suffix", ".png")
    batches = manifest["batches"]
    checks: list[dict] = []
    images: list[Path] = []

    expected_folders = {batch["target_folder"] for batch in batches}
    actual_folders = {
        path.name for path in target_root.iterdir()
        if path.is_dir() and path.name.startswith("#")
    }
    checks.append({
        "check": "campaign_folder_set",
        "passed": actual_folders == expected_folders,
        "missing": sorted(expected_folders - actual_folders),
        "unexpected": sorted(actual_folders - expected_folders),
    })

    for batch in batches:
        source = source_root / batch["source_folder"]
        target = target_root / batch["target_folder"]
        expected_count = int(batch["expected_count"])
        source_files = sorted(source.glob(f"{source_code} - *.png"))
        target_files = sorted(target.glob(f"{target_code} - *.png")) if target.exists() else []
        images.extend(target_files)
        expected_names = {
            path.name.replace(f"{source_code} - ", f"{target_code} - ", 1)
            for path in source_files
        }
        actual_names = {path.name for path in target_files}
        issues: list[str] = []
        if len(source_files) != expected_count:
            issues.append(f"source count {len(source_files)} != {expected_count}")
        if len(target_files) != expected_count:
            issues.append(f"target count {len(target_files)} != {expected_count}")
        if expected_names != actual_names:
            issues.append("source-to-target basename mapping mismatch")
        checks.append({
            "check": "batch",
            "source": batch["source_folder"],
            "target": batch["target_folder"],
            "expected_count": expected_count,
            "actual_count": len(target_files),
            "passed": not issues,
            "issues": issues,
            "missing": sorted(expected_names - actual_names),
            "unexpected": sorted(actual_names - expected_names),
        })

    hashes: dict[str, list[str]] = {}
    widths: list[int] = []
    heights: list[int] = []
    for path in images:
        issues: list[str] = []
        width = height = 0
        mode = ""
        if not path.name.startswith(f"{target_code} - "):
            issues.append("invalid target-language prefix")
        if not path.name.endswith(expected_suffix):
            issues.append("invalid filename suffix")
        if path.stat().st_size <= 50_000:
            issues.append("file size <= 50,000 bytes")
        try:
            with Image.open(path) as image:
                image.verify()
            with Image.open(path) as image:
                width, height = image.size
                mode = image.mode
        except Exception as exc:
            issues.append(f"PNG decode failed: {exc}")
        if mode != "RGB":
            issues.append(f"mode {mode} != RGB")
        if width < 900 or height < 1500:
            issues.append(f"resolution {width}x{height} below minimum")
        if height and not (0.53 <= width / height <= 0.59):
            issues.append(f"aspect ratio {width / height:.4f} outside tolerance")
        if width and height:
            widths.append(width)
            heights.append(height)
        sha = digest(path)
        hashes.setdefault(sha, []).append(str(path))
        checks.append({
            "check": "asset",
            "file": str(path),
            "passed": not issues,
            "issues": issues,
            "width": width,
            "height": height,
            "mode": mode,
            "bytes": path.stat().st_size,
            "sha256": sha,
        })

    duplicate_groups = [paths for paths in hashes.values() if len(paths) > 1]
    checks.append({"check": "duplicate_hashes", "passed": not duplicate_groups, "groups": duplicate_groups})
    expected_total = sum(int(batch["expected_count"]) for batch in batches)
    checks.append({"check": "total_count", "passed": len(images) == expected_total, "expected": expected_total, "actual": len(images)})

    ocr = {"attempted": False, "files": 0, "errors": 0, "recognized_lines": 0, "zero_line_files": 0}
    if manifest.get("ocr_path"):
        ocr_path = Path(manifest["ocr_path"])
        ocr["attempted"] = ocr_path.exists()
        if ocr_path.exists():
            data = json.loads(ocr_path.read_text(encoding="utf-8"))
            ocr.update({
                "files": len(data),
                "errors": sum(item.get("error") is not None for item in data),
                "recognized_lines": sum(len(item.get("lines", [])) for item in data),
                "zero_line_files": sum(not item.get("lines") for item in data),
            })

    failures = [check for check in checks if not check["passed"]]
    return {
        "language": manifest.get("language", target_root.name),
        "source_code": source_code,
        "target_code": target_code,
        "passed": not failures,
        "failure_count": len(failures),
        "image_count": len(images),
        "unique_hashes": len(hashes),
        "dimension_range": {
            "min_width": min(widths, default=0),
            "max_width": max(widths, default=0),
            "min_height": min(heights, default=0),
            "max_height": max(heights, default=0),
        },
        "ocr": ocr,
        "checks": checks,
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("manifest", type=Path)
    parser.add_argument("--report", type=Path)
    args = parser.parse_args()
    manifest = json.loads(args.manifest.read_text(encoding="utf-8"))
    report = verify(manifest)
    payload = json.dumps(report, ensure_ascii=False, indent=2) + "\n"
    if args.report:
        args.report.write_text(payload, encoding="utf-8")
    print(json.dumps({key: report[key] for key in ("language", "passed", "failure_count", "image_count", "unique_hashes", "dimension_range", "ocr")}, ensure_ascii=False, indent=2))
    return 0 if report["passed"] else 1


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: e6f8cf9888d253d9097177775d0f8d5a28ce0404d884de564f7c1ceaa7a685e9