PROCITY/pipeline/publish.py
m3ultra 8ad446a48a Lane E R41 (§41.1+§41.2): 46 clips and 110 fittings — and every handover number was the wrong unit
MOTION (§41.1). The clip library goes 8 -> 46: ten idles, eight browse, eight sit/lean,
eight social, six locomotion, six venue, in SIX grouped GLBs (one fetch each), 3.35 MB —
LESS than the 4.29 MB the old eight cost, via lossless dedup + int16 rotations (worst
error 0.0034 deg). All six verified skeleton-only (tris 0, meshes 0, nodes 66): ZERO DRAW,
which is what makes this round affordable. No retarget was run and none was wanted — the
bank and the peds are the same mixamorig skeleton, so retargeting would add error AND bake
the ped mesh into the clip, ending zero-draw. MIRPAMO 'make smoke' green to prove the tool,
then deliberately unused. Three seeds in the brief were duds and got substituted: the whole
*_Degree_Turn set is RIFLE-AIMING, and two 'examine' clips are 262 KB static poses, not
motion. R16 flat-body trap re-checked on all 46 (spine tilt 24 samples/clip, 0 frames >75
deg); turn_in_place auto-demoted from loopable on a 36.7 deg seam.

RULING 1 — the Bandai hazard, closed non-destructively and better than specified. 3,077 CC
BY-NC clips sat unzoned in a neutrally-named path. Renamed PER FILE (3,077/3,077) not just
the directory, because mirpamo names output <rig>@<clip>.glb — the _NC-research marker now
PROPAGATES into any retargeted GLB automatically. Nothing deleted; ultra's red/bandai
re-verified as canonical. Ledger corrected: the manifest claimed CC-BY-NC-ND, the bundled
licence text says CC BY-NC, no ND.

PROPS (§41.2). 110 assets from three libraries, published, sha1-verified, 0 validator errors
both modes. THE FINDING: every handover number was a TRIANGLE count, and triangles were
never the binding constraint — DRAW CALLS were. As handed over this cargo cost 14,140 draws
against a 162-draw margin; it ships at 117. '3dstore passes as-is' was true on tris/metres/
Draco/textures and FALSE on draws — 30 of 46 files carried up to 16 materials on one mesh
(one per record sleeve), so a 1,020-tri tub cost 16 draws; baked to COLOR_0, 288 -> 40, zero
tri drift, A/B identical. dj-gear's own manifest claimed median 6,288 tris / 18 under budget;
measured 51,528 and 12, with draws to 2,145. Pub props were NOT metre-correct (every source
unit-normalised to max dim 1.00 m) and 40x decimation was impossible as specified (loungeChair
is 94% non-manifold).

Ruling 5 vs the draw budget was a real conflict (one mixer = 4.6x a whole room); resolved by
joining SCENERY selectively while every control node keeps its object/name/pivot — verified
by parsing the SHIPPED GLB, not the tool that wrote it: PASS 4 / STATIC 10 / N/A 17 / FAIL 0.
deck_1200_rigged = 5 draws with Platter_SPIN, Arm_YAW, Fader_PITCH, Btn_STARTSTOP intact.

The R40 four-surface transmission gate FIRED ON LIVE CARGO: 5 genuinely transmissive
materials (cartridge dust-covers, a mixer meter window) that would each have doubled every
opaque draw in the room. 9 assets rejected on eyeball after decimation rather than shipping
mush. Two existing-tooling bugs fixed: normalize.py's yaw/up were silent no-ops (the jukebox
exported 0.40 m instead of 0.97 m), and footprints now measure the shipped GLB.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 18:10:12 +10:00

183 lines
9.0 KiB
Python

#!/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=<file> body=glb model/gltf-binary
* thumb POST {depot}/api/thumb?name=<file> 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():
if "--verify" in sys.argv: # drift-check the record vs the live depot, no publish
recpath = os.path.join(ROOT, "pipeline", "_published.json")
record = sorted(set(json.load(open(recpath)))) if os.path.exists(recpath) else []
print(f"_published.json: {len(record)} entries")
drift_check(record)
return
only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else ""
# [R41] --files LIST.txt : publish exactly these basenames (one per line). `--only` is a
# substring match, which cannot express "these 110 assets from three libraries and nothing
# else". It matters because `_normalized/` is shared: other lanes stage rigs and clips in the
# same directory, and a blanket run would push whatever they happen to have half-written.
explicit = None
if "--files" in sys.argv:
listfile = sys.argv[sys.argv.index("--files") + 1]
explicit = {ln.strip() for ln in open(listfile) if ln.strip()}
# Passwordless tailnet path (round-3 decision #4): GOD3_DEPOT overridden to an allowlisted
# tailnet endpoint (e.g. http://100.94.195.115:8788) trusts this box — no cookie needed.
tailnet = DEPOT != "https://digalot.fyi/3god"
glbs = sorted(g for g in glob.glob(NORM + "/*.glb")
if os.path.basename(g) not in SKIP
and (explicit is None or os.path.basename(g) in explicit)
and (not only or only in os.path.basename(g)))
if not glbs:
print(f"no GLBs in {NORM} matching {only!r} — run normalize.py first"); sys.exit(1)
if explicit is not None:
missing = explicit - {os.path.basename(g) for g in glbs}
if missing:
print(f"ERROR: {len(missing)} name(s) in --files are not staged in {NORM}: "
f"{sorted(missing)[:5]}")
sys.exit(2)
print(f"depot: {DEPOT} assets: {len(glbs)} {'(DRY RUN)' if DRY else ''}"
+ (" [tailnet passwordless]" if tailnet else ""))
if not DRY and not PW and not tailnet:
print("ERROR: GOD3_PW not set and no tailnet GOD3_DEPOT. Either:\n"
" export GOD3_DEPOT=http://100.94.195.115:8788 # passwordless from an allowlisted box\n"
" or export GOD3_PW=... # public depot (ask John)")
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:
# MERGE, never replace (decision #3): _published.json is the provenance record of everything
# ever pushed — a --only run must not clobber it to just this run's files (the R5 bug).
recpath = os.path.join(ROOT, "pipeline", "_published.json")
existing = set()
if os.path.exists(recpath):
try:
existing = set(json.load(open(recpath)))
except Exception:
pass
merged = sorted(existing | set(published))
json.dump(merged, open(recpath, "w"), indent=2)
print(f"\npublished {len(published)}/{len(glbs)} this run → "
f"_published.json now {len(merged)} (was {len(existing)}).")
drift_check(merged)
def drift_check(record):
"""Compare _published.json (the published-name RECORD) against the depot's actual file list.
A recorded name absent from the depot is real drift (deleted / never landed); a `procity_` file
live on the depot but unrecorded is a provenance gap. R17 ledger #4: the old check filtered the
depot list to `procity_*` and so false-flagged genuinely-published non-`procity_` assets
(sit.glb, the reused vintage-cash-register) as "not live" — now every published name is confirmed
by the name list itself, and only OTHER projects' non-`procity_` files on the shared depot are
ignored (they're expected, not ours)."""
try:
live = fetch_depot_files()
except Exception as e:
print(f" (drift check skipped — depot list unreachable: {e})")
return
rec = set(record)
missing_from_depot = rec - live # recorded but not live → real drift
unrecorded = {f for f in live if f.startswith("procity_")} - rec # our namespace live but unrecorded
if unrecorded:
print(f" DRIFT: {len(unrecorded)} procity_ on depot not in _published.json: "
f"{sorted(unrecorded)[:5]}")
if missing_from_depot:
print(f" DRIFT: {len(missing_from_depot)} in _published.json not live on depot: "
f"{sorted(missing_from_depot)[:5]}")
if not unrecorded and not missing_from_depot:
print(f" ✓ _published.json matches the live depot (all {len(rec)} recorded assets live).")
def fetch_depot_files():
"""EVERY file name on the depot (not prefix-filtered), so the drift check confirms any published
name — including non-`procity_` clips/reuses — is live by the name itself, not a prefix heuristic."""
import urllib.request as _u
req = _u.Request(DEPOT + "/api/list", headers={"User-Agent": "procity-validator/1.0"})
with _u.urlopen(req, timeout=15) as r:
d = json.loads(r.read().decode("utf-8"))
assets = d.get("assets", d) if isinstance(d, dict) else d
return {a["file"] for a in assets if a.get("file")}
if __name__ == "__main__":
main()