#!/usr/bin/env python3 """Publish the normalized PROCITY GLBs + thumbnails to the shared 3GOD depot. Mirrors the house push contract (meshgod/server.py:_push_3god) exactly: * auth cookie: 3g = hmac_sha256(GOD3_PW, "3god-ok").hexdigest() * GLB POST {depot}/api/upload?name= body=glb model/gltf-binary * thumb POST {depot}/api/thumb?name= body=png image/png Credentials come from the environment — this script never contains or logs the secret: export GOD3_PW=... # the shared depot password (ask John) export GOD3_DEPOT=https://digalot.fyi/3god # optional; this is the default python3 pipeline/publish.py # publish every GLB in _normalized/ python3 pipeline/publish.py --dry-run # list what WOULD upload, no network This is the one outward-facing step in the lane; run it deliberately. Uploads are idempotent (same name overwrites) and reversible via the depot's own delete. """ import os, sys, glob, hmac, hashlib, json, urllib.request, urllib.parse ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) NORM = os.path.join(ROOT, "pipeline", "_normalized") THUMBS = os.path.join(ROOT, "web", "assets", "thumbs") DEPOT = os.environ.get("GOD3_DEPOT", "https://digalot.fyi/3god").rstrip("/") PW = os.environ.get("GOD3_PW", "") DRY = "--dry-run" in sys.argv # assets intentionally excluded from publish (see AUDIT.md) SKIP = {"procity_fit_cube_shelf_01.glb"} def cookie(): tok = hmac.new(PW.encode(), b"3god-ok", hashlib.sha256).hexdigest() return f"3g={tok}" def to_jpeg(png_path): """PNG thumb → JPEG bytes (the depot's /api/thumb accepts jpeg only). Uses macOS sips (no deps); falls back to PIL if available elsewhere.""" import subprocess, tempfile if sys.platform == "darwin": with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as t: tmp = t.name try: subprocess.run(["sips", "-s", "format", "jpeg", "-s", "formatOptions", "85", png_path, "--out", tmp], check=True, capture_output=True) return open(tmp, "rb").read() finally: os.unlink(tmp) from PIL import Image # non-mac fallback import io buf = io.BytesIO() Image.open(png_path).convert("RGB").save(buf, "JPEG", quality=85) return buf.getvalue() def post(path, body, ctype): req = urllib.request.Request(DEPOT + path, data=body, method="POST", headers={"Content-Type": ctype, "Cookie": cookie()}) with urllib.request.urlopen(req, timeout=120) as r: return r.status, r.read().decode("utf-8", "replace")[:200] def main(): glbs = sorted(g for g in glob.glob(NORM + "/*.glb") if os.path.basename(g) not in SKIP) if not glbs: print(f"no GLBs in {NORM} — run normalize.py first"); sys.exit(1) print(f"depot: {DEPOT} assets: {len(glbs)} {'(DRY RUN)' if DRY else ''}") if not DRY and not PW: print("ERROR: GOD3_PW not set in environment. Ask John for the depot password, then:\n" " export GOD3_PW=... && python3 pipeline/publish.py") sys.exit(2) published = [] for g in glbs: name = os.path.basename(g) thumb = os.path.join(THUMBS, name.replace(".glb", ".png")) size = os.path.getsize(g) if DRY: print(f" would upload {name:38} {size//1024:>5}KB thumb={os.path.exists(thumb)}") continue try: st, msg = post("/api/upload?name=" + urllib.parse.quote(name), open(g, "rb").read(), "model/gltf-binary") # thumb is cosmetic — never fail the publish over it (house contract: # /api/thumb is keyed by the stored GLB name and wants JPEG) tstat = "" if os.path.exists(thumb): try: ts, _ = post("/api/thumb?name=" + urllib.parse.quote(name), to_jpeg(thumb), "image/jpeg") tstat = f" thumb:{ts}" except Exception as te: tstat = f" thumb:ERR({te})" ok = st in (200, 201) print(f" {'OK ' if ok else 'ERR'} {name:38} glb:{st}{tstat}") if ok: published.append(name) except Exception as e: print(f" ERR {name:38} {e}") if not DRY: json.dump(published, open(os.path.join(ROOT, "pipeline", "_published.json"), "w"), indent=2) print(f"\npublished {len(published)}/{len(glbs)} → depot. verify: curl -s {DEPOT}/api/list") if __name__ == "__main__": main()