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>
97 lines
4.0 KiB
Python
97 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Assert the DJ-gear named-node contract survives normalization — read from the GLB, not Blender.
|
|
|
|
python3 pipeline/check_contract.py SRC_DIR OUT_DIR MAP.json
|
|
python3 pipeline/check_contract.py --list FILE.glb [FILE.glb ...]
|
|
|
|
The contract 90sDJsim's GEAR driver reads (dj-gear/README.md) is five bare node names:
|
|
|
|
Platter_SPIN · Arm_YAW · Fader_PITCH · Fader_CROSS · Btn_STARTSTOP
|
|
|
|
Integrator ruling 5 exists because `gltf-transform optimize` silently deleted `Fader_CROSS` from a
|
|
696-node mixer. "Silently" is the operative word, so this check parses the **shipped GLB's** node
|
|
list rather than asking the tool that produced it — a scene-graph scan inside Blender still sees
|
|
control EMPTIES that were never exported, and would have reported a contract that the file does
|
|
not actually carry.
|
|
|
|
Verdict per model: PASS = every contract node present in the source is present in the output.
|
|
FAIL = one or more were lost (the ruling-5 failure mode).
|
|
N/A = the source carried none (a cartridge has no crossfader).
|
|
Exit 1 if any FAIL.
|
|
"""
|
|
import json, os, struct, sys
|
|
|
|
CONTRACT = ["Platter_SPIN", "Arm_YAW", "Fader_PITCH", "Fader_CROSS", "Btn_STARTSTOP"]
|
|
|
|
|
|
def glb_nodes(path):
|
|
"""Node names from a GLB's JSON chunk. Dependency-free — no Blender, no pygltflib."""
|
|
d = open(path, "rb").read()
|
|
if d[:4] != b"glTF":
|
|
raise ValueError("not a GLB")
|
|
off = 12
|
|
while off + 8 <= len(d):
|
|
clen, ctype = struct.unpack("<II", d[off:off + 8])
|
|
if ctype == 0x4E4F534A:
|
|
g = json.loads(d[off + 8: off + 8 + clen].decode("utf-8"))
|
|
return [n.get("name", "") for n in g.get("nodes", [])]
|
|
off += 8 + clen
|
|
raise ValueError("no JSON chunk")
|
|
|
|
|
|
def contract_of(path):
|
|
names = set(glb_nodes(path))
|
|
return [c for c in CONTRACT if c in names]
|
|
|
|
|
|
def main():
|
|
if "--list" in sys.argv:
|
|
for f in sys.argv[sys.argv.index("--list") + 1:]:
|
|
print(f"{os.path.basename(f):46} {','.join(contract_of(f)) or '-'}")
|
|
return 0
|
|
if len(sys.argv) < 4:
|
|
print(__doc__)
|
|
return 2
|
|
src_dir, out_dir, mapfile = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
pairs = json.load(open(mapfile)) # [{"src":"TTM54i.glb","file":"procity_...glb","mode":...}]
|
|
|
|
rows, fails = [], 0
|
|
for p in pairs:
|
|
s = os.path.join(src_dir, p["src"])
|
|
o = os.path.join(out_dir, p["file"])
|
|
if not (os.path.isfile(s) and os.path.isfile(o)):
|
|
rows.append({**p, "verdict": "MISSING"})
|
|
fails += 1
|
|
continue
|
|
before, after = contract_of(s), contract_of(o)
|
|
if not before:
|
|
verdict = "N/A"
|
|
elif p.get("mode") == "static":
|
|
# a static dressing copy is one joined mesh BY DESIGN — it is not claimed to be
|
|
# drivable, so a missing contract here is expected, not a failure.
|
|
verdict = "STATIC (contract intentionally dropped)" if not after else "STATIC"
|
|
elif set(before) <= set(after):
|
|
verdict = "PASS"
|
|
else:
|
|
verdict = "FAIL"
|
|
fails += 1
|
|
rows.append({**p, "contract_before": before, "contract_after": after, "verdict": verdict})
|
|
|
|
w = max(len(r["file"]) for r in rows) if rows else 40
|
|
print(f"{'shipped file':{w}} {'mode':7} {'source contract':46} {'shipped contract':46} verdict")
|
|
print("-" * (w + 110))
|
|
for r in rows:
|
|
print(f"{r['file']:{w}} {r.get('mode',''):7} "
|
|
f"{','.join(r.get('contract_before', [])) or '-':46} "
|
|
f"{','.join(r.get('contract_after', [])) or '-':46} {r['verdict']}")
|
|
n_pass = sum(1 for r in rows if r["verdict"] == "PASS")
|
|
n_na = sum(1 for r in rows if r["verdict"] == "N/A")
|
|
n_st = sum(1 for r in rows if str(r["verdict"]).startswith("STATIC"))
|
|
print(f"\nPASS {n_pass} · STATIC {n_st} · N/A {n_na} · FAIL {fails}")
|
|
json.dump(rows, open(os.path.join(out_dir, "_contract_check.json"), "w"), indent=2)
|
|
return 1 if fails else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|