← back to Wallco Ai

scripts/curate-monterey-batch.py

118 lines

#!/usr/bin/env python3
"""
curate-monterey-batch.py — READ-ONLY selector for the first hi-res Monterey
mural batch. Picks N published Monterey scenes spread across the hue wheel (so
the launch line has visual range, not 12 near-identical colorways) and writes a
manifest the operator feeds to build-mural-master.py.

NO writes to the DB, NO spend. Just a SELECT + a JSON manifest.

Usage:
  python3 scripts/curate-monterey-batch.py            # top 12 → data/mural-batch-monterey.json
  python3 scripts/curate-monterey-batch.py --n 25
"""
import argparse
import importlib.util
import json
from pathlib import Path

from psycopg2.extras import RealDictCursor

ROOT = Path(__file__).resolve().parents[1]
_spec = importlib.util.spec_from_file_location("buildtif", str(ROOT / "scripts" / "build-tif.py"))
_bt = importlib.util.module_from_spec(_spec); _spec.loader.exec_module(_bt)


def hue(hexstr):
    import colorsys
    s = (hexstr or "").lstrip("#")
    if len(s) != 6:
        return -1.0
    try:
        r, g, b = (int(s[i:i+2], 16) / 255 for i in (0, 2, 4))
    except ValueError:
        return -1.0
    mx, mn = max(r, g, b), min(r, g, b)
    if mx - mn < 0.06:
        return 1000 + (1 - mx)        # greys → end, light→dark
    return colorsys.rgb_to_hls(r, g, b)[0] * 360


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--n", type=int, default=12)
    ap.add_argument("--out", default=str(ROOT / "data" / "mural-batch-monterey.json"))
    a = ap.parse_args()

    conn = _bt.db()
    with conn.cursor(cursor_factory=RealDictCursor) as cur:
        cur.execute("""
            SELECT id, dominant_hex, local_path
              FROM all_designs
             WHERE category ILIKE 'monterey%' AND is_published
               AND local_path IS NOT NULL
             ORDER BY id DESC
        """)
        rows = [r for r in cur.fetchall() if Path(r["local_path"]).exists()]
    conn.close()

    # Exclude designs that ALREADY have a 12x11ft master built (on Henry / the
    # masters dir) so a "build more" batch never re-builds what's done.
    built = set()
    try:
        mdir = _bt.ROOT / "data" / "mural-masters"
        henry = Path("/Volumes/Henry/wallco-ai-archive/mural-masters")
        scan = henry if henry.is_dir() else mdir
        for d in scan.iterdir():
            if (d / "summary.json").exists():
                try: built.add(int(d.name))
                except ValueError: pass
    except Exception:
        pass
    before = len(rows)
    rows = [r for r in rows if r["id"] not in built]
    print(f"excluding {before - len(rows)} already-built masters; {len(rows)} candidates remain")

    # Spread across the hue wheel: bucket into N hue slots, take the newest
    # (already id-DESC) per slot, then fill any short slots from the remainder.
    rows.sort(key=lambda r: hue(r["dominant_hex"]))
    picks, seen = [], set()
    if rows:
        step = max(1, len(rows) // a.n)
        for i in range(0, len(rows), step):
            if len(picks) >= a.n:
                break
            r = rows[i]
            if r["id"] not in seen:
                picks.append(r); seen.add(r["id"])
        for r in rows:                      # backfill if buckets came up short
            if len(picks) >= a.n:
                break
            if r["id"] not in seen:
                picks.append(r); seen.add(r["id"])

    manifest = {
        "category": "monterey-mural",
        "n": len(picks),
        "spec": {"print_ft": 12, "height_ft": 11, "bleed_in": 6, "panels": [24, 36, 54]},
        "selection": "hue-spread across published Monterey (visual range)",
        "ids": [p["id"] for p in picks],
        "items": [{"id": p["id"], "dominant_hex": p["dominant_hex"],
                   "local_path": p["local_path"]} for p in picks],
        "batch_command": "for id in " + " ".join(str(p["id"]) for p in picks) +
                         "; do python3 scripts/build-mural-master.py $id; done",
        "estimate_command": f"python3 scripts/build-mural-master.py --estimate {len(picks)}",
    }
    Path(a.out).write_text(json.dumps(manifest, indent=2))
    print(f"curated {len(picks)} Monterey murals (hue-spread) → {a.out}")
    for p in picks:
        print(f"  #{p['id']:6}  {p['dominant_hex']}")
    print(f"\nbatch is ready — review then run:\n  {manifest['estimate_command']}\n"
          f"  {manifest['batch_command']}")
    return 0


if __name__ == "__main__":
    import sys
    sys.exit(main())