PROCITY/pipeline/clips_verify.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

156 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""PROCITY R41 §41.1 — gate: motion_manifest.json <-> web/models/clips/*.glb integrity.
python3 pipeline/clips_verify.py # 0 errors = green
python3 pipeline/clips_verify.py --stage <dir> # + quantisation fidelity vs the float source
Checks, all measured from the bytes (Lane F: this is the "every referenced clip resolves" gate):
1. every group file named in the manifest exists, and its byte size matches the recorded one
2. every clip id resolves to a NAMED animation inside its group GLB, and nothing extra rides along
3. every group GLB is ZERO-DRAW — 0 meshes, 0 materials, 0 images, no Draco
4. every group carries the same 66-node mixamorig skeleton (65 joints + Armature)
5. every animation channel targets a node that exists, on a path the manifest declares
6. recorded duration matches the max keyframe time in the file
7. `_rotOnly` survivability: every clip keeps >=1 non-Hips quaternion track, i.e. the clip still
says something after web/js/citizens/rigs.js filters it
8. with --stage: max angular error of the int16 rotation quantisation vs the float32 source
"""
import argparse
import json
import math
import os
import struct
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from clips_pack import acc_floats, canon_name, glb_read # noqa: E402
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--manifest", default="web/assets/motion_manifest.json")
ap.add_argument("--clips", default="web/models/clips")
ap.add_argument("--stage", help="staging dir of per-clip float32 GLBs (fidelity check)")
a = ap.parse_args()
man = json.load(open(a.manifest))
err, warn = [], []
total = 0
skel = None
for gfile, g in man["groups"].items():
p = os.path.join(a.clips, gfile)
if not os.path.exists(p):
err.append("%s: MISSING" % gfile)
continue
size = os.path.getsize(p)
total += size
if size != g["bytes"]:
err.append("%s: size %d != manifest %d" % (gfile, size, g["bytes"]))
js, bin_ = glb_read(p)
if js.get("meshes") or js.get("materials") or js.get("images"):
err.append("%s: NOT zero-draw (meshes=%d materials=%d images=%d)"
% (gfile, len(js.get("meshes", [])), len(js.get("materials", [])),
len(js.get("images", []))))
if any("draco" in e.lower() for e in js.get("extensionsUsed", [])):
err.append("%s: Draco present" % gfile)
names = [canon_name(n.get("name")) for n in js["nodes"]]
if skel is None:
skel = names
elif names != skel:
err.append("%s: skeleton differs from %s" % (gfile, list(man["groups"])[0]))
joints = [n for n in names if n.startswith("mixamorig:")]
if len(names) != 66 or len(joints) != 65:
err.append("%s: expected 66 nodes / 65 mixamorig joints, got %d / %d"
% (gfile, len(names), len(joints)))
anims = {an.get("name"): an for an in js.get("animations", [])}
if sorted(anims) != sorted(g["clips"]):
err.append("%s: animations %s != manifest clips %s"
% (gfile, sorted(anims), sorted(g["clips"])))
for cid in g["clips"]:
if cid not in anims:
continue
m = man["clips"][cid]
if m["group"] != gfile:
err.append("%s: clips[%s].group=%s" % (gfile, cid, m["group"]))
an = anims[cid]
dur, keeps = 0.0, 0
for ch in an["channels"]:
ni = ch["target"]["node"]
if not (0 <= ni < len(names)):
err.append("%s/%s: channel targets node %d out of range" % (gfile, cid, ni))
continue
if ch["target"]["path"] != "rotation" and man["_tracks"] == "rotation-only":
err.append("%s/%s: %s channel in a rotation-only build"
% (gfile, cid, ch["target"]["path"]))
if ch["target"]["path"] == "rotation" and not names[ni].endswith(":Hips"):
keeps += 1
t = acc_floats(js, bin_, an["samplers"][ch["sampler"]]["input"])
if t:
dur = max(dur, t[-1][0])
if abs(dur - m["duration"]) > 1e-3:
err.append("%s/%s: duration %.4f != manifest %.4f" % (gfile, cid, dur, m["duration"]))
if keeps == 0:
err.append("%s/%s: nothing survives rigs.js `_rotOnly`" % (gfile, cid))
if len(an["channels"]) != m["channels"]:
err.append("%s/%s: %d channels != manifest %d"
% (gfile, cid, len(an["channels"]), m["channels"]))
if total != man["totalBytes"]:
err.append("totalBytes %d != measured %d" % (man["totalBytes"], total))
if len(man["groups"]) > 6:
err.append("ruling 6: %d group files > 6" % len(man["groups"]))
if man["clipCount"] != len(man["clips"]):
err.append("clipCount mismatch")
# 8. quantisation fidelity against the float32 staging GLBs
if a.stage:
worst, worst_id = 0.0, None
for cid, m in man["clips"].items():
sp = os.path.join(a.stage, "%s.glb" % cid)
if not os.path.exists(sp):
warn.append("no staged source for %s" % cid)
continue
sj, sb = glb_read(sp)
gj_, gb = glb_read(os.path.join(a.clips, m["group"]))
snames = [canon_name(n.get("name")) for n in sj["nodes"]]
gnames = [canon_name(n.get("name")) for n in gj_["nodes"]]
ga = next(x for x in gj_["animations"] if x.get("name") == cid)
gmap = {gnames[c["target"]["node"]]: c for c in ga["channels"]
if c["target"]["path"] == "rotation"}
for ch in sj["animations"][0]["channels"]:
if ch["target"]["path"] != "rotation":
continue
bone = snames[ch["target"]["node"]]
if bone not in gmap:
continue
q0 = acc_floats(sj, sb, sj["animations"][0]["samplers"][ch["sampler"]]["output"])
q1 = acc_floats(gj_, gb, ga["samplers"][gmap[bone]["sampler"]]["output"])
if len(q0) != len(q1):
err.append("%s/%s: keyframe count %d != %d" % (cid, bone, len(q0), len(q1)))
continue
for x, y in zip(q0, q1):
if man["_encoding"].startswith("rotation outputs are int16"):
y = tuple(max(v / 32767.0, -1.0) for v in y)
d = abs(sum(p * q for p, q in zip(x, y)))
d /= (math.sqrt(sum(p * p for p in x)) * math.sqrt(sum(q * q for q in y)) or 1)
ang = math.degrees(2 * math.acos(max(-1.0, min(1.0, d))))
if ang > worst:
worst, worst_id = ang, "%s/%s" % (cid, bone)
print("quantisation: worst angular error %.6f deg (%s)" % (worst, worst_id))
if worst > 0.05:
err.append("quantisation error %.4f deg exceeds 0.05" % worst)
print("%d clips / %d groups / %d bytes (%.2f MB)"
% (man["clipCount"], len(man["groups"]), total, total / 1048576.0))
for w in warn:
print("WARN %s" % w)
for e in err:
print("ERROR %s" % e)
print("clips_verify: %s (%d errors, %d warnings)"
% ("GREEN" if not err else "RED", len(err), len(warn)))
return 1 if err else 0
if __name__ == "__main__":
sys.exit(main())