PROCITY/E-progress.md
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

1011 lines
72 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LANE E — ASSETS · progress (PROCITY-E)
*Owner: PROCITY-E. Scope: `pipeline/*`, `web/assets/*`. Nothing in other lanes' files was touched.*
> ⚠ **THIS FILE STOPS AT ROUND 12 AND HAS SINCE R13. IT IS NOT THE LIVE RECORD.** Everything from
> Round 13 on lives in **[`docs/LANES/LANE_E_NOTES.md`](docs/LANES/LANE_E_NOTES.md)** (what other
> lanes need from this one, newest first) and **[`pipeline/AUDIT.md`](pipeline/AUDIT.md)**
> (provenance, licence zone, measurements and method, per round). Recorded in R39 rather than
> quietly resurrected: a progress file whose last entry is 27 rounds old is a trap for the next
> reader, and the honest fix is a signpost, not a backfill. *(Two exceptions below: the R40 tasking
> explicitly asked for its control-arm transcripts in THIS file, and the R41 §41.2 tasking asked for
> its three import tables here, so §40 and §41 live here AND are cross-referenced from the notes.)*
## Round 41 (§41.2 — THE FITTINGS) · 2026-08-07
*Entry here by explicit R41 tasking (as R40 was). Cross-lane hand-off for C in
`docs/LANES/LANE_E_NOTES.md` §41; provenance in `pipeline/AUDIT.md`.*
**110 assets landed** from three libraries, all interior (ruling 4), all published + sha1-verified,
validator green. The round's one recurring lesson: **every handover number was a triangle count,
and triangles were never the binding constraint — draw calls were.**
| | source | shipped | rejected | tris | draws |
|---|---|---|---|---|---|
| `~/Documents/3dstore` | 46 | **40** | 6 | 18,420 | 288 → **40** |
| `not-tonight/art_incoming` | 48 | **39** | 3 (+6 `staff*` skipped) | 9,508,361 → 194,837 | 39 |
| ultra `3D=models/dj-gear` | 60 | **31** | 29 not selected | 2,284,094 → 143,340 | 13,813 → **38** |
| **total** | **154** | **110** | — | 11,810,875 → **356,597** | 14,140 → **117** |
### The finding that shaped the round: draws, not tris
The tasking said 3dstore "passes the house law AS-IS". On tris, metres, Draco and textures that is
exactly true and I re-verified all four. It is false on the number Lane C spends: **30 of the 46
files carry 316 materials on a single mesh** (one flat colour per record sleeve,
`Mat_Sleeve_<hex>`), and glTF gives each material its own primitive, which `GLTFLoader` turns into
its own `Mesh`. A 1,020-tri "Black Tub" cost **16 draw calls**. The interior margin is 162 — ten
tubs would have been the whole room.
Every one of those materials is a flat `baseColorFactor` with no texture and no UVs, so
`pipeline/merge_prims.py` bakes each primitive's colour into a `COLOR_0` attribute and emits one
white-factor material. glTF multiplies `COLOR_0` by `baseColorFactor` and both are linear, so the
result is **numerically the original colour** — verified by A/B render
(`docs/shots/laneE/r41_3dstore_merge_ab.png`) and by zero triangle drift on all 40.
The same defect, unmeasured, was in the DJ gear: joined bodies still carried 531 materials, so
"one mesh" was still 531 draws. `merge_prims.py` over that set took it **530 → 38**.
### §41.2a — `~/Documents/3dstore` · 40 shipped, 6 rejected
Re-measured with `glb_stat.py`, not quoted: 45 of 46 are ≤5k tris (median 384), metre-correct,
+Y up, no Draco, no textures. **44 of 45 are base-origin**`vinylsleeve_v5` is centred
(minY 0.160) and was re-seated. `3dmrp.glb` confirmed at **499,948 tris** + a 4096px texture.
No Blender pass: the geometry is already correct and every extra tool is a chance to move it.
Spec: `pipeline/r41_3dstore.json` (by name, ruling 3) · stager: `pipeline/r41_stage_3dstore.py`.
| id | kind | tris | draws | footprint (m) | height (m) |
|---|---|---:|---|---|---:|
| `beam_roof` | arch | 12 | 1 → 1 | 6.5 × 0.32 | 0.36 |
| `column_square` | arch | 12 | 1 → 1 | 0.22 × 0.22 | 2.6 |
| `column_wide` | arch | 12 | 1 → 1 | 0.36 × 0.46 | 2.6 |
| `door_shop` | arch | 144 | 1 → 1 | 1.7 × 0.19 | 2.3 |
| `bin_endoffunk` | bin | 972 | 16 → 1 | 0.36 × 0.34 | 0.34 |
| `chiu_bin` | bin | 996 | 16 → 1 | 0.37 × 0.43 | 0.35 |
| `chiu_bin_half` | bin | 600 | 16 → 1 | 0.24 × 0.43 | 0.35 |
| `chiu_bin_low` | bin | 912 | 16 → 1 | 0.34 × 0.43 | 0.35 |
| `crate_blue` | bin | 936 | 16 → 1 | 0.34 × 0.3 | 0.322 |
| `mos_bin_bottom` | bin | 912 | 16 → 1 | 0.34 × 0.4 | 0.34 |
| `mos_bin_lower_mid` | bin | 912 | 16 → 1 | 0.34 × 0.36 | 0.34 |
| `mos_bin_top` | bin | 912 | 16 → 1 | 0.34 × 0.2 | 0.34 |
| `mos_bin_upper_mid` | bin | 912 | 16 → 1 | 0.34 × 0.26 | 0.34 |
| `rack_bin_wooden` | bin | 912 | 16 → 1 | 0.34 × 0.4 | 0.338 |
| `tub_black` | bin | 1020 | 16 → 1 | 0.36 × 0.53 | 0.229 |
| `tub_black_long` | bin | 1536 | 16 → 1 | 0.53 × 0.36 | 0.229 |
| `tub_white` | bin | 972 | 16 → 1 | 0.34 × 0.53 | 0.229 |
| `tub_white_long` | bin | 1536 | 16 → 1 | 0.53 × 0.36 | 0.229 |
| `counter_station` | counter | 192 | 1 → 1 | 0.88 × 0.5 | 0.87 |
| `counter_station_side` | counter | 192 | 1 → 1 | 0.5 × 0.88 | 0.87 |
| `vinyl_sleeve` | prop | 180 | 4 → 1 | 0.32 × 0.01 | 0.32 |
| `window_booth` | prop | 144 | 1 → 1 | 1.85 × 0.09 | 0.72 |
| `bin_big_4x4` | rack | 288 | 4 → 1 | 0.59 × 1.5 | 1.255 |
| `chiu_rack_long` | rack | 300 | 3 → 1 | 1.88 × 0.43 | 0.795 |
| `chiu_rack_short` | rack | 276 | 3 → 1 | 1.38 × 0.43 | 0.795 |
| `mos_stack` | rack | 204 | 5 → 1 | 0.37 × 0.4 | 1.275 |
| `tubs_3x3` | rack | 168 | 3 → 1 | 0.53 × 1.3 | 0.555 |
| `tubs_3x3_long` | rack | 168 | 3 → 1 | 1.3 × 0.53 | 0.51 |
| `tubs_4x2_vertical` | rack | 240 | 3 → 1 | 0.53 × 1.56 | 0.475 |
| `tubs_4x2_wide` | rack | 240 | 3 → 1 | 1.56 × 0.53 | 0.475 |
| `tubs_shelf_long` | rack | 228 | 3 → 1 | 1.56 × 0.55 | 0.565 |
| `tubs_shelf_wide` | rack | 228 | 3 → 1 | 0.55 × 1.56 | 0.575 |
| `wooden_rack_long` | rack | 384 | 3 → 1 | 0.78 × 1.8 | 0.81 |
| `wooden_rack_wide` | rack | 384 | 3 → 1 | 1.8 × 0.78 | 0.93 |
| `library_trolley` | trolley | 264 | 4 → 1 | 0.41 × 0.88 | 0.74 |
| `aircon_unit` | wall_mount | 12 | 1 → 1 | 1.15 × 0.25 | 0.35 |
| `shelf_board_long` | wall_mount | 12 | 1 → 1 | 4.6 × 0.48 | 0.03 |
| `shelf_board_short` | wall_mount | 12 | 1 → 1 | 0.48 × 1.95 | 0.03 |
| `wall_shelves_big` | wall_mount | 36 | 3 → 1 | 4.65 × 0.05 | 0.76 |
| `wall_shelves_long` | wall_mount | 48 | 4 → 1 | 2.6 × 0.05 | 1.1 |
**Rejected (6):**
| source | why |
|---|---|
| `3dmrp.glb` | 499,948 tris (100x the 5k prop budget) + a 4096px texture. Named in the tasking as the one reject; measured and confirmed. |
| `DOOR.glb` | 0.010 m thick — a 1 cm plane, 12 tris. The solid door is DOOR-front.glb (0.19 m), which is accepted as door_shop. |
| `GREY COLUMN.glb` | 0.010 m thick plane, 12 tris. The solid post is GREY COLUMN-FRONT.glb (0.22 x 0.22 m), accepted as column_square. |
| `Shelf Level.glb` | 4.50 x 0.05 x 0.005 m — a 5 mm trim rail from the booth_room build script, not a placeable fitting. |
| `Shelf Level long.glb` | 2.60 x 0.05 x 0.005 m — same 5 mm trim rail, short version. |
| `floor crate.glb` | 0.34 x 0.34 x 0.002 m — a 2 mm floor decal. Would z-fight Lane C's interior floor; a real crate is crate_blue. |
### §41.2b — DJ gear · 31 shipped, node contract 4/4 PASS
**Ruling 5 and the draw budget were in direct conflict, and nobody had measured the second half.**
`gltf-transform optimize` is banned because it deleted `Fader_CROSS` — correct. But the reason it
was reached for is also real: measured on ultra's exports, these models cost **TR-1000 2,145 ·
MPC-XL 1,319 · TR-808 1,092 · PMC-08Pro 751 · TTM54i 659 draws**. Against a 162-draw margin, one
PMC-08Pro is 4.6× an entire room. Both "join everything" and "join nothing" ship something
unusable.
*(Also re-measured: the handover's "median 6,288 tris, 18 already ≤5k" is the stale
`dj-gear/manifest.json`. True median is **51,528 tris**; only **12** of 60 are ≤5k.)*
`pipeline/rig_join.py` joins **selectively**: scenery merges to one body, each contract node keeps
its own object, name and pivot. Two variants ship — `static` (dressing, 1 draw) and `rigged`
(drivable hero, 25 draws). Contract verified by `pipeline/check_contract.py`, which parses the
**shipped GLB's** node list rather than trusting the tool that wrote it.
| id | mode | tris | draws | source contract | shipped contract | verdict |
|---|---|---|---|---|---|---|
| `deck_1200_rigged` | rigged | 254,048 → 3,821 | 62 → 5 | Platter_SPIN,Arm_YAW,Fader_PITCH,Btn_STARTSTOP | Platter_SPIN,Arm_YAW,Fader_PITCH,Btn_STARTSTOP | PASS |
| `mixer_pmc05pro_rigged` | rigged | 61,872 → 5,000 | 525 → 2 | Fader_CROSS | Fader_CROSS | PASS |
| `mixer_shdj1200_rigged` | rigged | 86,577 → 4,999 | 776 → 2 | Fader_CROSS | Fader_CROSS | PASS |
| `mixer_ttm54i_rigged` | rigged | 75,788 → 5,000 | 659 → 2 | Fader_CROSS | Fader_CROSS | PASS |
| `cart_case` | static | 6,912 → 5,000 | 79 → 1 | — | — | N/A |
| `cart_concorde` | static | 2,853 → 2,853 | 34 → 1 | — | — | N/A |
| `cart_m447` | static | 1,387 → 1,387 | 25 → 1 | — | — | N/A |
| `cart_stanton` | static | 3,290 → 3,290 | 15 → 1 | — | — | N/A |
| `deck_1200` | static | 254,048 → 5,000 | 62 → 1 | Platter_SPIN,Arm_YAW,Fader_PITCH,Btn_STARTSTOP | — | STATIC (contract intentionally dropped) |
| `drum_808` | static | 118,569 → 5,000 | 1092 → 1 | — | — | N/A |
| `drum_tr1000` | static | 119,959 → 4,897 | 2145 → 1 | — | — | N/A |
| `headphones_hd25` | static | 8,495 → 4,999 | 169 → 1 | — | — | N/A |
| `headshell_technics` | static | 2,097 → 2,097 | 30 → 1 | — | — | N/A |
| `mixer_mx2200` | static | 69,127 → 5,000 | 411 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
| `mixer_pmc05pro` | static | 61,872 → 5,000 | 525 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
| `mixer_pmc06pro` | static | 59,633 → 5,000 | 506 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
| `mixer_pmc07pro` | static | 64,752 → 4,999 | 420 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
| `mixer_pmc08pro` | static | 137,885 → 5,000 | 751 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
| `mixer_pmc10` | static | 72,950 → 4,999 | 522 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
| `mixer_pmc20` | static | 91,127 → 5,000 | 775 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
| `mixer_pmx9000` | static | 130,053 → 5,000 | 807 → 1 | — | — | N/A |
| `mixer_shdj1200` | static | 86,577 → 5,000 | 776 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
| `mixer_ttm54i` | static | 75,788 → 4,999 | 659 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
| `sampler_mpc3000` | static | 102,536 → 5,000 | 480 → 1 | — | — | N/A |
| `sampler_s900` | static | 86,536 → 5,000 | 443 → 1 | — | — | N/A |
| `sampler_sp1200` | static | 158,218 → 5,000 | 481 → 1 | — | — | N/A |
| `sl_deck_1` | static | 19,139 → 5,000 | 113 → 1 | — | — | N/A |
| `sl_deck_2` | static | 19,241 → 5,000 | 97 → 1 | — | — | N/A |
| `sl_deck_3` | static | 23,347 → 5,000 | 141 → 1 | — | — | N/A |
| `sl_deck_4` | static | 19,875 → 5,000 | 161 → 1 | — | — | N/A |
| `zip_drive` | static | 9,543 → 5,000 | 72 → 1 | — | — | N/A |
**`STATIC` = the contract is dropped by design** (a dressing copy is one joined mesh and is not
claimed to be drivable); **`N/A`** = the source carried no contract node (a cartridge has no
crossfader). **FAIL: 0.** Excluded as out-of-period: DIGITAKT-II, DIGITONE-II, POLYEND-DRUMS,
MPC-XL (2020s gear); the rest of the 60 are near-duplicate colourways.
### §41.2c — pub/venue props · 39 shipped, 3 rejected, 6 `staff*` skipped
Two handover claims failed re-measurement. **These are not metre-correct**: every source is
unit-normalised to a max dimension of exactly 1.00 m, so a picnic table and an ashtray are the same
size. Aspect *is* preserved, so a real-world height (read off each `*_shot.png` reference render)
fixes all three axes — the height table is `pipeline/r41_pub_batch.json`. And **textures are not
"all fine"**: 37 of 39 have no texture at all (they carry `COLOR_0` vertex colour), while `keg` and
`urinal_trough` carried 2048px maps, resized to 1024.
**40× decimation was impossible as specified.** COLLAPSE decimation cannot merge across overlapping
shells: `loungeChair` is **94% non-manifold** (376,076 of 399,542 verts) and a ratio-0.01 decimate
moved it 1,319,836 → 1,274,119, i.e. **3%**. `topiary` behaved identically at 93%; `pokie`, at 29%,
decimated normally. So `normalize.py` gained a `remesh` path (voxel remesh → decimate → vertex-colour
transfer), which needed two fixes found by measuring rather than assuming:
- **colour came back empty** until `bpy.ops.object.datalayout_transfer()` created the destination
layers — a Data Transfer modifier applied without it silently transfers nothing;
- **a voxel remesh needs a volume**, so thin-walled sources (`keg`) returned a wire cage at every
voxel size until a Solidify pass preceded the remesh
(`docs/shots/laneE/r41_shellsoup_solidify.png`).
| id | src tris | out tris | ratio | non-manifold | footprint (m) | height (m) |
|---|---:|---:|---:|---:|---|---:|
| `ash_urn` | 198,782 | 5,000 | 40× | 43% | 0.32 × 0.33 | 0.75 |
| `ashtray_post` | 198,710 | 5,000 | 40× | 42% | 0.41 × 0.41 | 0.95 |
| `bain_marie` | 184,992 | 4,958 | 37× | 83% | 0.89 × 0.76 | 0.95 |
| `banquette` | 190,350 | 4,999 | 38× | 68% | 1.55 × 1.02 | 0.85 |
| `beer_umbrella` | 192,752 | 4,995 | 39× | 60% | 2.68 × 2.65 | 2.3 |
| `bin_round` | 197,576 | 4,999 | 40× | 40% | 0.53 × 0.51 | 0.75 |
| `bistro_table` | 196,995 | 4,999 | 39× | 38% | 1.18 × 1.18 | 0.92 |
| `broken_basin` | 197,978 | 5,000 | 40× | 53% | 0.59 × 0.49 | 0.6 |
| `cable_snake` | 193,623 | 5,000 | 39× | 64% | 0.45 × 0.47 | 0.2 |
| `can_stack` | 196,005 | 5,000 | 39× | 46% | 0.38 × 0.32 | 0.45 |
| `cdj_deck` | 197,967 | 4,998 | 40× | 33% | 0.63 × 0.42 | 0.15 |
| `champ_bucket` | 183,943 | 4,999 | 37× | 62% | 0.24 × 0.2 | 0.35 |
| `concrete_plinth` | 190,528 | 5,000 | 38× | 53% | 0.76 × 0.74 | 1.2 |
| `crowd_barrier` | 189,524 | 5,000 | 38× | 60% | 1.97 × 0.3 | 1.1 |
| `distro_box` | 193,633 | 5,000 | 39× | 41% | 0.63 × 0.87 | 1.2 |
| `fire_ext` | 198,707 | 5,000 | 40× | 37% | 0.34 × 0.19 | 0.6 |
| `firepit` | 188,041 | 5,000 | 38× | 88% | 0.76 × 0.56 | 0.5 |
| `flight_case` | 195,452 | 5,000 | 39× | 37% | 0.6 × 0.36 | 0.75 |
| `jukebox` | 184,271 | 4,999 | 37× | 75% | 0.97 × 0.41 | 1.52 |
| `keg` | 441,861 | 5,000 | 88× | 23% | 0.4 × 0.4 | 0.6 |
| `lounge_chair` | 1,319,156 | 5,000 | 264× | 94% | 0.69 × 0.69 | 0.8 |
| `marble_sink` | 199,095 | 4,998 | 40× | 21% | 1.15 × 0.56 | 0.85 |
| `old_couch` | 196,277 | 5,000 | 39× | 46% | 1.8 × 0.95 | 0.85 |
| `pallet` | 198,086 | 5,000 | 40× | 30% | 0.79 × 0.73 | 0.2 |
| `picnic_table` | 197,370 | 4,999 | 39× | 42% | 1.38 × 1.41 | 0.75 |
| `pie_warmer` | 196,434 | 5,000 | 39× | 31% | 0.61 × 0.71 | 0.55 |
| `planter` | 239,862 | 4,897 | 49× | 87% | 0.98 × 0.96 | 0.55 |
| `plywood_bar` | 192,907 | 5,000 | 39× | 63% | 1.51 × 0.82 | 1.05 |
| `pokie` | 198,030 | 5,000 | 40× | 29% | 0.75 × 0.78 | 1.7 |
| `raffle_drum` | 193,830 | 5,000 | 39× | 68% | 0.37 × 0.37 | 0.9 |
| `rope_post` | 199,748 | 5,000 | 40× | 27% | 0.32 × 0.32 | 1.0 |
| `sandwich_board` | 198,359 | 5,000 | 40× | 25% | 0.58 × 0.56 | 1.0 |
| `smoke_machine` | 194,423 | 5,000 | 39× | 52% | 0.17 × 0.24 | 0.38 |
| `speaker_stack` | 186,357 | 5,000 | 37× | 59% | 0.52 × 0.52 | 1.8 |
| `strobe_light` | 193,392 | 5,000 | 39× | 57% | 0.22 × 0.24 | 0.4 |
| `sub_bass` | 193,581 | 4,998 | 39× | 53% | 0.87 × 0.63 | 0.7 |
| `topiary` | 415,872 | 4,999 | 83× | 93% | 0.61 × 0.61 | 1.2 |
| `urinal_trough` | 488,808 | 5,000 | 98× | 6% | 0.31 × 0.26 | 0.7 |
| `wheelie_bin` | 195,084 | 5,000 | 39× | 42% | 0.72 × 0.92 | 1.1 |
**Rejected (3) — eyeballed against each source's `*_shot.png`, per the house rule:**
| id | src tris | why |
|---|---:|---|
| `balustrade` | 198,229 | glass panel + handrail. The remesh lost the handrail entirely and left a lumpy slab with a detached lump — a transparent thin panel is the worst case for a signed-distance remesh. Eyeballed against balustrade_shot.png; unusable. |
| `cocktail_bar` | 194,298 | back-bar with a lit bottle display. At 5k the bottle shelving smears into a grey L-shaped mass; nothing about it reads as a bar. Rejected rather than shipping mush. |
| `dj_plinth` | 197,159 | the white plinth survives but the controller on top melts into a grey blob. Nothing is lost: cdj_deck normalized cleanly and Lane C can stand it on any counter or the plywood_bar. |
Plus the **6 `staff*.glb` skipped** as instructed (200k-tri statues, no skin/anim/material — and
PROCITY has a rigged ped fleet, so a photogrammetry human statue would read wrong beside it).
### Gate, publish, validate
**The R40 four-surface transmission gate earned its keep on live imports, not a fixture.** Sweeping
the 110 new assets caught **5 real `KHR_materials_transmission` materials** — cartridge dust-covers
(`cc_clear`, `n44_clear`, factor 0.850.9) and a mixer meter window (`meter_window`, 0.9). Each
would have made three.js issue *every opaque draw in that room twice*. `strip_transmission.py`
removed all 5 with **BIN chunk byte-identical** (sha1 printed per file). This is the control arm
R40 demonstrated with a synthetic fixture, firing on real cargo.
Full house-law sweep over all 110 — tris ≤5k, WebP ≤1024, no Draco, base-origin |minY| ≤5 mm, no
transmission, and `_normalized/` byte-identical to the `web/assets/models/` served mirror:
**0 violations.**
```
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/publish.py --files pipeline/_r41_publish_list.txt
published 110/110 this run → _published.json now 169 (was 59).
✓ _published.json matches the live depot (all 169 recorded assets live).
$ sha1 verify (fetch each back from the depot, compare bytes)
110/110 identical, 0 mismatched # → pipeline/_r41_sha1.json
32.4 MB on the depot
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py
manifest v1 — facades 49, skins 92 groups, GLBs 149 (+40 non-manifest GLBs transmission-swept), audio 34
OK — 0 errors, 0 warning(s) # rc=0
$ ... validate_manifest.py --depot # stricter: every GLB must be live on the depot
OK — 0 errors, 0 warning(s) # rc=0
```
`manifest.json` fittings **23 → 133**. Every new entry carries `footprint` [w,d] and `height` in
metres measured **from the shipped GLB** (not from the Blender scene that wrote it — `rig_join`'s
in-scene bounds mis-reported a 0.114 m mixer as 0.629 m, which is why the staging step re-measures
the file), plus `kind`, `tris` and `draws` so Lane C can spend the 162-draw margin deliberately.
DJ entries add `mode`, `control_nodes`, `source_controls` and `drivable`.
### Tools added / changed (all in `pipeline/`)
| file | what |
|---|---|
| `merge_prims.py` | **new** — bake `baseColorFactor``COLOR_0`, collapse N primitives to 1. The draw-cost fix. |
| `contact_sheet.py` | **new** — labelled multi-GLB sheet in one Blender run, the eyeball tool. |
| `thumbs.py` | **new** — EEVEE 256px thumbs; workbench `TEXTURE` renders vertex-coloured, material-less meshes flat grey, which is 2 of this round's 3 libraries. |
| `rig_join.py` | **new** — the selective join; static + contract-preserving rigged variants. |
| `check_contract.py` | **new** — contract assertion read from the shipped GLB. |
| `normalize.py` | `remesh` / `shell_thickness` / `vertex_colour` options + `nonmanifold_ratio()`; explicit `COLOR_0` export; **orientation bug fixed**`yaw`/`up` were applied without re-selecting the object, so `transform_apply` silently no-opped (the jukebox came out 0.40 m wide instead of 0.97 m). Now it re-selects and *verifies the dimensions changed*, warning if not. |
| `publish.py` | `--files LIST.txt` — publish an explicit set; `_normalized/` is shared with other lanes and a blanket run would push their half-written staging. |
| `build_manifest.py` | reads the three R41 result files; new entries carry kind/tris/draws/control-nodes. |
| `r41_3dstore.json`, `r41_pub_batch.json`, `r41_djgear_batch.json` | the by-name specs (ruling 3). |
### Proof shots (`docs/shots/laneE/`)
`r41_3dstore_40.png` · `r41_3dstore_merge_ab.png` (16-material source vs 1-draw merge, identical) ·
`r41_pub_42.png` · `r41_pub_before_after.png` · `r41_pub_rejects.png` (the 3 rejects beside their
sources) · `r41_shellsoup_solidify.png` (the keg cage vs the solidify fix) · `r41_djgear_31.png` ·
`r41_djgear_rigged.png` · `r41_djgear_merge_ab.png`.
### For Lane C (§41.4)
Everything is in `manifest.json` under `fittings`, keyed by the ids in the tables above. Place on
`footprint`/`height` untouched. Budget notes: **every static asset is 1 draw**; the four `*_rigged`
DJ heroes are 25 draws each and are the only ones with drivable `control_nodes`. The `-long`/
`-wide` 3dstore pairs are the same mesh rotated 90° — pick one, they are not two props. `kind`
tells you what it stands on: `bin`/`rack`/`counter`/`trolley` are floor-standing, `wall_mount`
needs a wall and a height, `arch` is structure, `prop` is a surface dressing.
## Round 40 (§40.1+40.4) · 2026-08-04 — *(entry here by explicit R40 tasking; cross-lane story in `docs/LANES/LANE_E_NOTES.md` §40)*
### §40.1 — transmission gate blind spot CLOSED, both control arms demonstrated
`pipeline/validate_manifest.py` `check_transmission()` now covers every path the game can load a
GLB from: `pipeline/_normalized/` (as before), **`web/assets/models/`** (the `?localdepot=1` served
mirror — never scanned before), **the depot copy** for any manifest GLB with no local file in either
dir (ranged GET of the GLB JSON chunk; 206-aware, slices a 200 — the tailnet depot returns 200-full),
and a **sweep of every non-manifest `.glb`** in both dirs (ped rigs, dance clips — `rigs.js` loads
them by literal path). A depot fetch failure is a loud WARN (`depot copy UNSCANNED`), never a skip.
**Fixture:** `procity_fit_box_crate_01.glb` with `transmissionFactor: 1` injected into material[0]
(the exact defect `strip_transmission.py` removes; injector kept in the session scratchpad, BIN
untouched). Clean sha1 `6c327d8624bc90203275a341128abb546f5d69e0` (staging = served, verified after
cleanup). **Depot arm used a LOCAL MOCK depot (`python3 -m http.server` serving `a/<file>`) — no
fixture ever touched the real depot namespace.**
**RED/GREEN arm 1 — the `web/assets/models/` path** (transmissive copy planted at the served path
ONLY; staging copy clean — the R39 gate is structurally blind here):
```
$ python3 $SCRATCH/make_glass.py pipeline/_normalized/procity_fit_box_crate_01.glb \
web/assets/models/procity_fit_box_crate_01.glb
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/.validate_r39_ab.py # R39 copy of the gate
OK — 0 errors, 0 warning(s) # old rc=0 ← the blind spot, live
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py # R40 gate, same tree
ERR procity_fit_box_crate_01.glb (assets/models copy): material[0] 'Material.001' has
transmissionFactor 1 — three.js will run a transmission PRE-PASS ... # new rc=1 ← RED
$ cp $SCRATCH/hold/procity_fit_box_crate_01.glb web/assets/models/ # remove fixture
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py
OK — 0 errors, 0 warning(s) # rc=0 ← GREEN
```
**RED/GREEN arm 2 — the depot path** (BOTH local copies moved aside = the R39 live-demo shape:
manifest GLB, no local file anywhere; mock depot on 127.0.0.1:8991 serves the transmissive copy):
```
$ mv pipeline/_normalized/procity_fit_box_crate_01.glb $SCRATCH/hold/norm_...
$ mv web/assets/models/procity_fit_box_crate_01.glb $SCRATCH/hold/models_...
$ GOD3_DEPOT=http://127.0.0.1:8991 python3 pipeline/validate_manifest.py
ERR procity_fit_box_crate_01.glb (depot copy): material[0] 'Material.001' has
transmissionFactor 1 — ... # rc=1 ← RED
# mock now serves the CLEAN bytes (local still absent):
$ GOD3_DEPOT=http://127.0.0.1:8991 python3 pipeline/validate_manifest.py
OK — 0 errors, 38 warning(s) # rc=0 ← GREEN (mock)
# and the REAL tailnet depot, local still absent — the production fetch path, scanning the R39-fixed copy:
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py
OK — 0 errors, 0 warning(s) # rc=0 ← GREEN (real depot)
```
**Cleanup + final:** both local copies restored (sha1 `6c327d86…` both), mock killed, temp R39
script deleted, fixtures gone. Full validator on the real tree:
`manifest v1 — facades 49, skins 92 groups, GLBs 39 (+40 non-manifest GLBs transmission-swept),
audio 34 · OK — 0 errors, 0 warning(s) · rc=0`.
### §40.4 — the arcade's posts: re-measured, and the verdict is a PLACEMENT RULE, not an asset
**Re-measured on today's tree** (`node pipeline/arcade_measure.mjs` + `buildings.js` read at the
cited lines — all R39 numbers stand): one edge `kind:'arcade'` **42.00 m × 5 m**, lot faces ±2.50 m
**5.00 m lane**; `AWNING_DEPTH 2.2` (`buildings.js:21`) at `AWNING_Y 2.95` (`:22`) ⇒ slabs reach
±0.30 m ⇒ **0.60 m open slot** at the centreline; posts (`:641-646`) at `d/2 + 2.2 0.2` = 2.00 m
from each lot face = **±0.50 m from the centreline — two rows 1.00 m apart**, 0.12 × 0.12 × 2.95 m,
**34 posts** at seed 20261990 (2 × 17 shop lots), colliderless (`:649`).
**The kit (posts + roof designed together): the roof SPANS (depth 2.5 = half-lane, slabs meet flush
at the centreline), and a spanned roof has NO posts.** That is a change in `web/js/world/buildings.js`
+ one line of district plumbing in `web/js/world/chunks.js`**Lane B's files, so E edited nothing**
and handed the exact rule (with the mandatory explicit `?classic=1` gate — the synthetic IS the
classic boot) to B in `LANE_E_NOTES.md` §40 with file:line.
**Proof shots without touching B's tree:** the rule was applied **in-flight** by a rewriting
no-store dev server (session scratchpad `patch_server.py`, exact-match rewrite that fails loudly if
the source moved) and shot headless (Playwright, `DBG.teleport(21.5, 66.67, −π/2)` — inside the lane
at seed 20261990, looking down it):
`docs/shots/laneE/r40_arcade_posts_before.png` (post rows + open slot) ·
`r40_arcade_posts_after.png` (clear lane, continuous roof) · `r40_arcade_posts_pair.png`.
**HUD draws 124 → 124 (+0), tris 44k → 44k, chunks 25 → 25.** Nothing generated, published, or
re-published this round; disk copies of `buildings.js`/`chunks.js` byte-identical throughout.
## Round 12 (v3.0-alpha gig layer, `?gigs=1`) · 2026-07-15
Three asset streams for one-pub-end-to-end, all free/on-device. C works the pub interior in parallel
(I touched none of `web/js/interiors/*`).
- **Instruments (5 GLBs)** — `flux_local` concept → `trellis_mac``normalize.py`, via
`gen_props.py --batch gig_instruments.json`. **All 5 kept** (electric_guitar, bass_guitar, drum_kit,
guitar_amp, mic_stand) — every one reconstructed cleanly (thin mic-boom + cymbal hardware survived,
unlike the round-3 wire rack). Heavy tris (3096k, TRELLIS shell-soup — amp 96k the outlier),
accepted for focal/LOD-able stage props; manifest fittings + thumbnails + staged to local-depot.
D's rigs hold them; primitive fallback if absent.
- **Posters (4 skins)** — `gen_skins.py --posters`: poster-xerox/screenprint/grunge/retro, portrait
flyers with a **blank name-band** (no baked text); `skins.poster[].nameZone` [x,y,w,h] for B/C to
overprint the seeded band name — same discipline as blank-signboard facades. Eyeballed, zero rejects.
- **Gig audio (3)** — extended `gen_audio.py` procedurally: `music-pubrock-live` (original 90s pub-rock
riff), `ambience-crowd-walla` (crowd murmur), `sfx-applause` (cheer sting). All `gig:true` in the
manifest for F. Pack now 26 assets, 9.1 MB of 25. Verified (spectrogram + seams/peaks).
- **Manifest**: `skins.poster` + audio gig assets + 5 instrument fittings; `build_manifest.py` reads
`_gig_results.json`; `validate_manifest.py` covers audio. Provenance in `AUDIT.md` (§ Round 12).
- **Open — depot publish gated**: the 5 instrument GLBs are staged locally (usable via `?localdepot=1`)
but the tailnet depot push was **blocked by the harness classifier** (outward-facing) → **needs
John's OK**. Until then `validate --depot` / qa Gate 3 flag provenance-drift on the 5. F owns the
v3.0-alpha tag; I did not commit yet pending the publish call.
## Round 11 (AUDIO round — E leads) · 2026-07-15
Stood up the audio pack, free + on-device. **Tool: 100% procedural synthesis** (numpy → ffmpeg) —
picked over neural gen because it's $0, deterministic/seeded, perfectly loopable, and parody-safe by
construction (documented in README § audio, per Fable's ask).
- **`pipeline/gen_audio.py`** — a small numpy DSP engine (oscillators, ADSR, filters, reverb,
seamless-loop crossfade, a note sequencer + drum synth). 23 assets, seeded, `--dry-run`/`--only`.
- **`web/assets/audio/`** — 6 ambience beds (street day/night, rain, 3 interior roomtones), 13 sfx
(doorbell, door, till, record-riffle, toast, tram bell + rumble, 6 footsteps), 4 instrumental 90s
music loops (record-shop, milkbar, video-synth, arcade). OGG/Opus + M4A/AAC. **8.1 MB** of 25 MB.
- **Manifest** `audio:{ambience,sfx,music}` — files + m4a fallback + loop + gain; interior roomtone/
music carry `types[]` for seed-pick per shop (like facades); footstep is `{surface:[variants]}`.
`build_manifest.py` builds it, `validate_manifest.py` checks it. **qa green, audio 23, 0 errors.**
- **Verified** technically (I synthesise, I can't subjectively *listen*): spectrogram sheet
`docs/shots/laneE/audio_spectrograms.png` + numeric — peaks ≤ -0.4 dBFS (no clip), **all loops
seamless**, durations correct. **Subjective ear-check is John's/F's** (flagged honestly).
- Provenance in `AUDIT.md` (§ Round 11). House audio law respected on the E side: silent-happy
(manifest lists only shipped files), lazy-load metadata, seeded flavour. B wires WebAudio, C gives
the `room.audio` contract, F integrates + owns the **v2.1 tag/push** — I did **not** push.
---
*Round 3 and earlier below.*
*Reviewed against `docs/LANES/LANE_E_ASSETS.md` + `docs/LANES/ROUND3_INSTRUCTIONS.md`.*
## Round 9 (Fable) — VRAM audit + depot hygiene (ship-round DD)
- **E1 — VRAM/texture audit: DONE + committed.** Measured every texture source (decoded RGBA+mips).
Resident VRAM: **~240 MB default boot** (skins-in-view ~85 + ped fleet ~90 + GLB ~25 + Lane-B sign
atlas ~40), **~420 MB worst case** (all flags + all 3 shop types). **Cheap win taken:** half-res'd
book+toy atlases (quick pull-cards; record kept full for the deep dig; covers verified legible) →
**stock VRAM 323→181 MB**, no index/UV change, atlases re-published. **Verdict: fine to ship v2.0**
— no compression/lazy-load needed for the tag. Flagged for v3: stockpack cache-cap (C) + KTX2/Basis.
- **E2 — depot/manifest hygiene: confirmed green.** `validate --depot` 0 err · pack-QA 3/3 · provenance
-drift clean (`_published.json` == 27 live) · 15 stock atlases live, **0 orphans**.
- **On-call:** flagged box-primitive-first tram rec to B; audio still parked on John.
## Round 8 (Fable) — pack kitchen (book/toy) + pack-QA gate
- **E1 — pack pipeline generalized + book/toy sample packs.** `build_stock_pack.py` now kind-
parametrized (record 256×256, **book 96×256 tall-thin spines**, toy 256×256), per-kind parody word
banks, same laws. Book/toy sample packs built + staged (`?localdepot`) + indexes committed to
`pipeline/packs/` so **Lane C wires book/toy shelves now**. Parody verified deterministic + no
distinctive-token leakage across all 3 kinds.
- **E2 — pack-QA gate: DONE.** `validate_pack.py` (wired into `validate_manifest.py` → qa gate 3):
a bad bake (out-of-range/inverted UV, empty title, bad price_band, dangling atlas, dup id) **fails
qa** — proven (inverted-UV+empty-title → exit 1). qa `--strict` GREEN 5/5.
- **Tags pushed** (John-delegated): origin/main = HEAD (1a20501) + v1.0/v1.1/v2.0-alpha/v2.0-beta all
live on origin — verified.
- **REAL packs LIVE (John unblocked DB access).** All 3 packs rebuilt from GODVERSE (m1ultra
Postgres clones, read-only SELECT, peer auth): **record 350** (`discogs_full.inventory` — John's
real store stock + real Discogs covers), **book 311** + **toy 273** (`dealgod.products` by
category, real product photos + prices). Metadata parody-transformed (no real trademarks); covers
are real art (decision #2); book prices seeded (info-only source). 15 atlases published to the
depot, indexes committed to `pipeline/packs/` + staged, all pass the pack-QA gate. `build_stock_pack.py`
gained `--from-tsv` (extract on the DB box → build here) + portrait book cell + seeded-price
fallback. Access path: deployment DB was Docker-internal/SSH-denied → used the m1ultra clones per
John. No fal spend.
- **On-call** (B rain-streak/puddle, D door-swing tell): none requested.
## Round 7 (Fable) — the two feeders (ped merge + stock pack)
- **E1 — ped sub-mesh merge: DONE + committed** (`447188a`, ref `lane-e/round7-peds`). All 19 rigs
merged to **ONE draw each** (1 prim + 1 atlased 1024 material) via `pipeline/merge_ped.py` (Blender
join → UV-pack → EMIT-bake all materials into one atlas). Skinning + 65-joint skeleton preserved,
`walk`/`idle` untouched. **Fleet 92→19 draws; near-cap 116→24 (79% cut).** Verified in the vendored
GLTFLoader (all load as SkinnedMesh + bind walk; man_suit poses byte-identical to original; 3-mat
man_athletic atlas correct). Handed to **Lane D (D1)** for full validation + full-density numbers →
gates F's roster flip. qa `--strict` GREEN 5/5.
- **E2 — GODVERSE stock pack: pipeline BUILT + PROVEN, real pack blocked on 2 external deps.**
`pipeline/build_stock_pack.py` = recordgod → curate → **parody-transform (deterministic, zero
real-trademark leakage: "Dark Side of the Moon"/"Pink Floyd" → "Quiet Milk Bar"/"The Galahs")** →
atlas WebPs + JSON index. Proven end-to-end with `--sample 24`; **sample pack staged** to
`web/assets/models/` so Lane C wires `?stock=real` against the real index format now. **Real 200-400
pack awaits (1) `GODVERSE_DSN` (dealgod Postgres creds — flagged to Fable/John) and (2) C's schema
confirmation** (proposed in LANE_E_NOTES). One command (`--from-db --count 300`) once they land.
- **E3 — on-call** (B rain-streak/puddle texture): none requested. No fal spend (balance ~$21.36).
## Round 6 (Fable) — non-destructive pipeline + on-call
- **E1 — publish.py non-destructive + provenance-drift gate: DONE + committed.**
- `publish.py` now **merges** into `_published.json` (read existing → union → sorted write),
never replaces — fixes the R5 `--only` clobber (record dropped 27→1). **Proven:** a real no-op
re-publish of `record_crate` kept the record at **27 (was 27)**, not 1.
- Post-publish + a new `--verify` mode **drift-check** the record against the live depot
(`/api/list` filtered to `procity_*`) and warn on either direction. `--verify` confirms 27 == 27.
- `validate_manifest.py` gains a **provenance-drift gate**: every manifest `procity_*` GLB must be
in `_published.json`, else QA fails. **Proven:** simulating the R5 clobber (record→1) makes the
validator exit 1 with per-file drift errors; restored → green. So the clobber class fails loudly.
- **Record verified**: `_published.json` (27) is byte-exact vs the live depot — Fable's R5
regeneration was correct, no fix needed.
- `qa.sh --strict` GREEN (5 gates incl. F1's new v2-flags harness).
- **On-call for B/C** (task 3): available for bus_shelter/bin scale-origin tweaks (B) and
glass_case/magazine_rack in-room issues (C). Broken-prop fal re-gen pre-authorized ≤2 attempts
(decision #5) — none needed yet. No speculative generation.
## Round 5 (Fable) — hero-prop bake + fal fallbacks + depot growth
- **E1 — hero-prop bake-to-lowpoly: DONE + committed.** New `pipeline/bake_lowpoly.py` (Blender
headless): voxel-remesh → decimate → **Cycles base-colour bake** onto fresh UVs (R3's lesson:
simplify-only floors on shell-soup; the re-bake is the point). All **6 heavy TRELLIS props ≤8k
tris** (drinks_fridge 77k→8k, milkshake_mixer 73k→8k, listening_booth 54k→8k, counter_till
32k→8k, novelty_record 26k→8k, arcade_cabinet 25k→8k). **Fleet 288k→48k tris, 18M→5.2M.** Each
eyeballed (`docs/shots/laneE/props_baked.png` — fridge needed a coarser voxel re-bake, rest clean)
+ verified loading in the vendored GLTFLoader. Re-published; `validate --depot` 0 err. Originals
gitignored in `pipeline/.props_orig/`. → Lane C re-measures the record interior (was ~273k).
- **E3 — fal.ai fallbacks: DONE.** glass_case + bus_shelter + magazine_rack via fal Hunyuan3D v2
(`pipeline/gen_props_fal.py`, key from MODELBEAST settings). Frosted/opaque concepts (`flux_local`)
gave the reconstructor a surface — all 3 succeeded where TRELLIS failed. Baked to ≤8k (EMIT albedo
bake — Hunyuan materials need base-colour→emission or DIFFUSE bakes near-black), verified loading,
published, `validate --depot` 0 err. **Total fal spend ~$0.64** (glass 1 att, bus 2 att, mag 1
att × ~$0.16), well under the <$1 estimate; guardrail held. Costs logged in AUDIT R5. Last two
primitive-only fittings (glass_case, magazine_rack) retired; new street `bus_shelter`. Manifest
now 17 fittings + 9 furniture (26 GLBs). Contact sheet `docs/shots/laneE/props_fal.png`.
- **E2 — depot growth: DONE (light, consumer-driven).** The v2 features are procedural (dig uses
generated sleeves, B's window shader + D's roster need no GLBs), so per Fable "no unconsumed
entries" I added two clear-consumer props: `street/bin` (council-bin → Lane B) + `fit/crate_stack`
(milk-crate-stack → Lane C floor piles/dig). Both 3k tris, re-normalized + published, verified
loading, `validate --depot` 0 err. Also **resolved Lane B's B3 ask** — novelty_record 26.5k→8k
via the E1 bake. Manifest now **18 fittings + 10 furniture (28 GLBs)**. Further growth on request.
## Round 4 (Fable) — ped decimation (gate-3 critical path) + asset fixes
- **E1 — ped decimation: DONE + committed** (`518678d`, ref `lane-e/round4-peds`). The 19 ped rigs
in `web/models/peds/` were **924k tris total** (31k56k each) — the gate-3 blocker. Decimated to
**≤3k each** (15642805; fleet **924k→46.5k**, ~2.45k avg) via `gltf-transform weld → simplify
--ratio 0.05 --error 0.12` (meshoptimizer — works on these manifold skinned meshes, unlike R3
TRELLIS shell-soup). **Skinning + skeleton fully preserved** (65 joints intact, `walk`/`idle`
untouched); contact sheet `docs/shots/laneE/peds_decimated.png` clean at street distance. Originals
gitignored in `pipeline/.peds_orig/`; per-ped in `pipeline/_peds_decim.json`. → **Lane D validates
(D2)** then **Lane F re-measures gate 3** (~24-cap × ≤3k ≈ ≤67k fleet contribution).
- **E2a — `record_crate` FIXED, verified + published.** Lane C's `undefined 'uri'` crash was a
**sourceless texture** (packed AO/roughness map failed WebP encode; material's metallicRoughness+
occlusion slots pointed at it). New tool `pipeline/fix_glb_textures.py` strips those slots (three.js
loads textures lazily → no crash). **Verified it loads in the real vendored `GLTFLoader`** (meshes=1,
tris=5000). **Re-published to the depot** (the fixed version is live, tex refs all-sourced).
- **E2b — counter-with-till (~2 m): LIVE.** New `counter_till` fitting
(`procity_fit_counter_till_01.glb`, **1.61 × 0.64 m, 1.15 m tall**, 32k tris) — timber counter +
cabinet doors + period till. Generated on-device (`flux_local` concept → `trellis_mac`, 3 concept
rolls to land the ~2:1 aspect), normalized to house law, **verified loads in `GLTFLoader`**, and
**published**. Non-destructive (keeps the balcao long-counter as `counter`); manifest
`fittingAliases.counter``counter_till`; Lane C re-maps kind `counter``counter_till` in `glb.js`.
- **Publish: DONE** (John approved). Both re-exports live; **manifest `validate --depot` GREEN
(0 err, 23 GLBs live)**. `publish.py` gained an `--only <name>` filter + tailnet-passwordless
detection (`GOD3_DEPOT=http://100.94.195.115:8788`, decision #4).
- **Not doing** (per Fable decisions): fal.ai spend (glass-case/bus-shelter parked); hero-prop
bake-to-lowpoly.
## Round 3 (Fable) — hero props on MODELBEAST + local-depot mode
- **Hero props, free & on-device** (`pipeline/gen_props.py`): `flux_local` concept image → `trellis_mac`
image→GLB (1024_cascade) → `normalize.py` → thumbs → manifest. **5 generated + kept**
(listening_booth, drinks_fridge, arcade_cabinet, milkshake_mixer, novelty_record — contact sheet
`docs/shots/laneE/props_gen_local.png`), **cash_register reused** from the depot
(`vintage-cash-register.glb`). 3 didn't make it: **magazine_rack** → in-engine primitive (112k-tri
wire, the thin-wire gotcha); **glass_case + bus_shelter****fal.ai fallback list** (TRELLIS can't
do transparent glass; sf3d gives a featureless blob) — gated on John, ≤$2.70, both have primitive
fallbacks. Every kept thumbnail eyeballed; no legible branding (see AUDIT provenance).
- **Local-depot mode** (`core/loaders.js` `?localdepot=1` + `pipeline/stage_local_depot.py`): resolves
`depot:<file>` to `web/assets/models/` served by the dev server, so Lanes B/C/F validate GLB loading
**with no network / before publish**. Verified live in-browser (loads a `depot:` GLB, CDN path intact).
- **Manifest**: 14 fittings + 8 furniture (5 hero + cash_register added); `validate_manifest.py`
green (0 err). Provenance recorded in `AUDIT.md` (props are 100% synthetic/local — cleanest in the set).
- **Known tradeoff**: TRELLIS meshes are dense shell-soup — decimate floors 2577k tris (heavier than
the ≤5k library law). Accepted for focal/optional/LOD-able props; texture-bake-to-lowpoly is a follow-up.
- **Published (self-serve, John's go 2026-07-14)**: the 5 prop GLBs + thumbs pushed to the depot via
the passwordless tailnet path (decision #4); all 22 GLBs re-verified live. **`qa.sh --strict` GREEN**
(4/4 gates; manifest validator 0 err / 0 warn with the props live). Provenance was recorded first.
- **Not committed**: this round is **held for Fable review** (per John) — all Lane E round-3 changes are
in the working tree, uncommitted. GLB binaries stay git-ignored/staged; the depot has them.
## Status at a glance
| Step | Deliverable | State |
|---|---|---|
| 1 Audit | `pipeline/AUDIT.md` (+ `glb_stat.py`, `_audit_stats.json`) | ✅ done |
| 2 Normalize | `pipeline/normalize.py`**16 GLBs** + 16 thumbs, verified | ✅ done (local); ⛔ **publish gated on `GOD3_PW`** |
| 3 Manifest | `web/assets/manifest.json` + `validate_manifest.py` (+ `build_manifest.py`) | ✅ done, validates green |
| 4 Facade skins | `gen_skins.py` **`--local`** backend + 23-skin gap pack | ✅ **DONE — 23 skins generated on-device, free, harvested + mapped** |
| 5 Hero props | round-3 `gen_props.py` (MODELBEAST, free) → **5 local** + cash-register reuse | ✅ **DONE local** (see Round 3 above; 2→fal, 1→primitive) |
| 6 Local-depot | `loaders.js ?localdepot=1` + `stage_local_depot.py` | ✅ **DONE, verified in-browser** |
| — | `pipeline/README.md` + contact sheets in `docs/shots/laneE/` | ✅ done |
**Step 4 no longer costs anything.** The 23 facades were generated **locally on the M3 Ultra**
via MODELBEAST `flux_local` (FLUX.2-klein-4B on MPS) — free, ~5.7s each, weights already cached.
No OpenRouter key, no ~9¢ spend. See "Step 4 (done, local)" below.
**Two things still need John (both optional — the game runs asset-free):** (1) the depot password
`GOD3_PW` to publish the 16 GLBs live; (2) a go/no-go on the paid **hero-prop** step (5 ≈ $2.70).
## What landed
### Step 1 — Audit
- `pipeline/glb_stat.py`: dependency-free GLB parser (tris, bbox, texture px, Draco) — runs on
ultra's py3.9. Full library stats → `pipeline/_audit_stats.json`.
- `pipeline/AUDIT.md`: the library is a raw dump — almost nothing obeys the GLB law (only
`record_crate_plastic_01` was native-metres; scale/orientation arbitrary; tris 12→960k; textures
to 4096; several **branded/trademarked** assets quarantined). Candidate→PROCITY-id table with
status, coverage-vs-registry table, QUARANTINE list, depot cross-reference.
- Toolchain reality documented (Blender present on ultra but off PATH; no coreutils/timeout).
### Step 2 — Normalize & publish
- `pipeline/normalize.py`: headless Blender, extends `MESHGOD/finish_glb.py` (solidify/decimate/
bright/base-origin) + adds **height-normalize, outlier cleanup, texture→WebP≤1024, per-asset
up/yaw, 256px thumbnail**. Driven by `pipeline/batch.json`.
- **16 assets** normalized to house law (metres, +Y up, base origin, ≤5k tris, WebP ≤1024, no
Draco). Every one **eyeballed** — Y-up confirmed by re-importing exports through `render_glb.py`.
Footprints from Blender-space `_normalize_results.json`. Thumbs in `web/assets/thumbs/`.
- fittings (9): record_crate, wire_shelf, clothes_rack, bookshelf, cube_shelf_wide, counter,
coffee_table, work_table, box_crate
- furniture (7): bench, bench_modern, bench_wood, longbench, park_bench, streetlight, food_cart
- `pipeline/publish.py`: mirrors the meshgod→3GOD contract exactly (cookie `hmac(GOD3_PW,"3god-ok")`,
`POST /api/upload` + `/api/thumb`). Reads the secret from env — never contains/logs it. `--dry-run`
verified all 16 ready. **Not published yet: `GOD3_PW` isn't set here.** Run: `export GOD3_PW=… &&
python3 pipeline/publish.py` (idempotent, reversible).
### Step 3 — Manifest
- `web/assets/manifest.json` (v1): the contract Lanes B/C/D read. All **69 skins** catalogued
(25 facade / 10 sky / 10 ground / 8 wallpaper / 6 floor / 7 surface / 3 awning) + 16 fittings/
furniture with **footprints, heights, thumbs**. `conventions` block documents the GLB law,
blank-signboard rule, corner-lot policy, fallbacks, and depot load path.
- Every registry shop type has **≥2 facades** (verified in the validator). Facades tagged
`signboard: blank|baked` — 5 djsim/market/grimy skins carry baked sign text and must not get a
name overlay; the other 20 are clean overlay targets.
- `pipeline/validate_manifest.py`: Lane F gate — parses JSON, checks every skin exists, every GLB
is local-or-on-depot, thumbs exist, footprints/heights sane, ≥2 facades/type. **Green** (16
local-only warnings until publish; `--depot` enforces live).
- `pipeline/build_manifest.py`: reproducible generator (curated type-maps + derived skin/fitting
lists) so the manifest regenerates when assets change.
### Step 4 — Facade skin expansion (DONE, local)
- `pipeline/gen_skins.py`: ported from `thriftgod/gen_assets.py` — **same STYLE bible + blank-
signboard template** so new skins match. Resumable `.genraw/`, `--dry-run`/`--only`/`--harvest`.
- **`--local` backend (the one I used):** MODELBEAST `flux_local` → FLUX.2-klein-4B on the M3
Ultra's MPS GPU. Apache/**ungated**, weights already cached (15 GB), **free**, ~5.7 s/img, one
GPU job at a time. Direct subprocess to `~/Documents/MODELBEAST/server/operators/flux_local/run.py`
(`--outdir`/`--params` JSON) — no server, no cloud, no key. Cloud OpenRouter path kept as fallback.
- **All 23 generated → harvested → mapped.** 0 failures, 2:11 total. Eyeballed every one
(contact sheet `docs/shots/laneE/facades_gen_local.png`): every shopfront has a **genuinely
blank signboard** (Lane B name-overlay target), style indistinguishable from the 25 anchors.
The pack: toy (2), book (2), video (2), milkbar (3), wide dept anchors (2), stall/market (2),
warehouse (2), arcade portal (1), residential house fronts (4), corner-lot `-side` walls (3).
- **Coverage after mapping** (`build_manifest.py` `FACADE_TYPES`, mirrors registry.js + additive):
record 3 · opshop 3 · toy 5 · book 5 · video 4 · pawn 7 · milkbar 5 · dept 4 · **stall 4**
— every registry type ≥3. **`stall` went 2→4**, retiring the Lane A single-facade gap.
- **Corner-lot `-side` skins now exist** (brick/render/weatherboard): tagged `face:"side"` with
empty `types` in the manifest so no shop-front pool ever picks a windowless wall; Lane B maps
them to the exposed side face (`conventions.corner_lots` + new `conventions.facade_face`).
- `pipeline/SKIN_PROMPTS.md`: identical pack kept for the free manual flow (fallback if MODELBEAST
is ever down).
### Step 5 — Bespoke hero props
- `pipeline/meshgod_batch.json` + `MESHGOD_BATCH.md`: 9 solids (glass case, fridge, till, listening
booth, arcade cabinet, magazine rack, milkshake mixer, novelty record, bus shelter) in the
thriftgod_hero pattern. Documents the **MeshGod gotcha** (thin wires/flat discs → primitives),
reuse-first (cash-register/glass-case likely already on depot), and the primitive fallbacks.
**Not run** — gated on John (~$2.70). Nothing blocks: all have in-engine fallbacks.
## Adversarial review & fixes (self-run, 4-dimension × verify)
Ran a 19-agent adversarial review over all deliverables (correctness / contract-fit / doc-accuracy /
house-law), each finding independently verified. **15 confirmed** (1 blocker, 5 major, 9 minor).
All fixed and re-verified:
- **BLOCKER — off-origin exports.** `normalize.py` joined meshes but never flattened parent node
hierarchies, so assets under a parent empty shipped off-origin: `park_bench` was **~1 km off-origin,
floating 30 m**; `food_cart` 15 m off; `clothes_rack` sank 0.19 m. My earlier spot-check missed it
(the clean assets have no hierarchy). **Fixed:** `parent_clear(KEEP_TRANSFORM)` + drop non-mesh
nodes before join. Re-verified node-aware: all 16 now `center=[0, h/2, 0]`, **minY=0.0**.
- **Major — tri budget counted faces, not triangles** (latent for quad FBX/OBJ). Fixed: triangulate
in `clean_mesh` so the ≤5k gate + reported counts are true triangles.
- **Major — fitting-id namespace.** Added `fittingAliases` (registry semantic id → physical id) to
the manifest. (Lane C's `glb.js` already maps its kinds to my physical ids, so it worked — this
makes it explicit + future-proof.)
- **Major — facade map vs registry.js.** Aligned my facade→type map to be a consistent superset of
Lane A's `registry.js`; flagged that registry's `stall` pool has only 1 facade (Lane A gap).
- **Major — facing -Z not enforced.** Corrected the docs: symmetric props are orientation-agnostic;
no auto-facing (per-asset `yaw` available). Removed the false "enforced" claim.
- **Major — licence provenance.** Strengthened the publish-set caveat (record per-asset provenance
before pushing to the shared CDN) — a reason publish stays gated on John.
- **Minors:** hardened `glb_stat.py` (node-aware world bounds + WebP size sniff — the exact blind
spots that hid the blocker); `validate_manifest.py` liveness now uses a Range GET not HEAD;
Akai-MPC trademark flag; corrected audit counts (59 parsed `.glb`, rejects 8, food_cart tris).
## Acceptance check (LANE_E)
-`AUDIT.md` complete.
- ✅ ≥15 normalized GLBs (**16**) with thumbnails — ⛔ *live on 3GOD pending publish (needs cred)*.
-`manifest.json` validates green; fittings/furniture carry footprints for B/C to place untouched.
- ✅ Every registry shop type has ≥2 facades — now **≥3** (48 facades total). New skins
**generated** (not just planned) and eyeball-matched to the anchor style
(`facades_style_anchor.png` = 25 originals; `facades_gen_local.png` = 23 new local skins;
`fittings_normalized.png` = the 16 GLBs — all in `docs/shots/laneE/`).
- ✅ One-page `pipeline/README.md` (re-run each step, costs, where creds live).
## Notes for the other lanes
- **Lane B / C:** read `web/assets/manifest.json`. `fittings`/`furniture` → `depot:<file>` via
`core/loaders.js` (placeholder box on miss — the game runs asset-free). Footprints are `[w,d]`
metres on the ground; heights in metres; all base-origin, +Y up, facing Z.
- **Lane B:** overlay shop names only on facades with `signboard:"blank"`. Wide `dept-*` anchors
(when generated) want a wider UV plane. `awning`/`ground`/`sky` skins are yours; `wall`/`interior`
are Lane C's.
- **Lane F:** wire `python3 pipeline/validate_manifest.py` into the gate (add `--depot` post-publish).
## Open questions for Fable/John
1. **Publish now?** I have 16 verified GLBs staged in `pipeline/_normalized/` ready to push to the
shared depot. It's an outward-facing write to shared infra + needs `GOD3_PW`, so I left it for
your go rather than assume it.
2. ~~Spend the ~9¢ on facades?~~ **Resolved — done for free on-device** (MODELBEAST flux_local).
Only the **~$2.70 hero-prop** MeshGod batch (Step 5) still wants a go/no-go; all props have
in-engine primitive fallbacks, so it's a polish upgrade, not a blocker. *(Note: image→GLB is
also local on this box via `sf3d`/`trellis_mac`, but those weights are HF-gated — a setup
step before that path is free too.)*
3. A few library assets were near-misses (garage_shelves, radiogram, market_stall — see AUDIT
rejects). Worth a manual Blender re-origin pass later, or leave to primitives? I left them out.
---
# Round 41 (§41.1) — THE MOTION LIBRARY *(Lane E, motion half)*
**Done, green, and it is the round's critical path — Lane D can start.** 46 clips, 6 grouped GLBs,
**3 498 124 B (3.34 MB)**, zero-draw, plus a new `web/assets/motion_manifest.json`.
Full provenance + the licence lines are in `pipeline/AUDIT.md` under "Round 41 §41.1".
## 0. Ruling 1 first — the Bandai hazard is closed (paths, exactly)
| | |
|---|---|
| **was** | `~/Documents/MIRPAMO/library/downloads/bandai_namco_1/` — 3,077 **CC BY-NC 4.0** BVH, 725 MB, unzoned, neutrally named, **nothing in any filename marking them**. One `mirpamo batch` from a shipped build. |
| **now** | `~/Documents/MIRPAMO/library/downloads/red/bandai_namco_1_NC-research/` |
| **and** | every clip renamed: **3,077 / 3,077** now end `_NC-research.bvh`; `find … ! -name '*_NC-research.bvh'` returns **0** |
| **new** | `~/Documents/MIRPAMO/library/downloads/red/README.md` — the zone law, mirrored from ultra's `MOTIONLIB/red/README.md` |
| **fixed** | `~/Documents/MIRPAMO/library/manifest.json`: `CC-BY-NC-ND 4.0`**`CC BY-NC 4.0`** (+ `zone:"RED"`, `verified:true`, the renamed path as `name`, and the correction recorded in `notes`) |
**NOTHING WAS DELETED.** 552 MB of clip data is still on disk byte-for-byte; the instruction
preferred non-destructive and ultra holds the canonical zoned copy anyway
(`~/Documents/MOTIONLIB/red/bandai/`, re-verified live: **3,077 BVH / 552 MB**).
Why per-file and not just the directory: `bin/mirpamo` `cmd_batch` names output
`<rig>@<clipbasename>.glb`, so **the marker now propagates into any retargeted GLB automatically**.
`downloads/` is gitignored in MIRPAMO, so `red/` inherits that.
The ledger fix is real, not cosmetic: the bundled `dataset-1/LICENSE` and `dataset-2/LICENSE` both
open *"Attribution-NonCommercial 4.0 International"***no ND clause**, as ultra's `SOURCES.md`
already said. Also recorded while I was in there: `downloads/lafan1_ubisoft/` (the other
non-commercial source) is **scaffold only on this box** — 332 KB of 134-byte git-lfs pointers,
**0 BVH**, so it cannot leak; the real payload is ultra's `red/lafan1/`.
## 1. `mirpamo make smoke` — the tool ground truth, run BEFORE the batch
**PASSED**, Blender 5.1.2, exit 0 — procedural mesh → autorig → retarget → verify, `[mirpamo] verify OK`.
## 2. What ships
`web/models/clips/` (new dir), one fetch per group — ruling 6 is **6 files, not 46**:
| group file | category | clips | bytes | motion |
|---|---|---:|---:|---:|
| `idles.glb` | idle | 10 | 1 029 496 | 78.3 s |
| `social.glb` | social | 8 | 895 648 | 73.9 s |
| `venue.glb` | venue | 6 | 526 036 | 39.5 s |
| `browse.glb` | browse | 8 | 476 056 | 30.8 s |
| `sitlean.glb` | sitlean | 8 | 358 112 | 27.5 s |
| `locomotion.glb` | locomotion | 6 | 212 776 | 12.1 s |
| **TOTAL ADDED** | | **46** | **3 498 124 B = 3.34 MB** | **262 s** |
`web/assets/motion_manifest.json`**16 KB** (new file; I did not touch `manifest.json`, which is
the props half's this round). **Total added bytes: 3 514 636 (3.35 MB).**
The 8 clips PROCITY already ships cost **4.30 MB**. This is **5.75× the clips for 78% of that**.
## 3. ZERO-DRAW — proved with `glb_stat.py`, not asserted
```
browse.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
idles.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
locomotion.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
sitlean.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
social.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
venue.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
```
66 nodes = 65 mixamorig joints + `Armature`, i.e. the **byte-shape of the existing `idle.glb`**
(which is 66 nodes / 1 skin / 0 meshes / 0 tris). Confirmed again after loading in three.js:
0 meshes, 0 triangles added to the scene by any group file.
## 4. THE MANIFEST SCHEMA — `web/assets/motion_manifest.json` (Lane D reads this)
**One line: load `groups[<file>]` once, then index `clips[<clipId>]` — the group GLB's
`gltf.animations[]` are NAMED by `clipId`, so `animations.find(a => a.name === clipId)` is the whole
lookup.**
```jsonc
{
"_schema": "…", "_licence": "…", // provenance travels with the data
"_tracks": "rotation-only", // see §5 — this is what rigs.js keeps anyway
"_encoding": "rotation outputs are int16 NORMALIZED …; all 65 joints animated incl. fingers",
"_note": "skeleton-only, 0 tris, 66 nodes — same byte-shape as models/peds/idle.glb",
"fps": 30,
"groups": { // ONE ENTRY = ONE FETCH
"idles.glb": { "category":"idle", "desc":"…", "clips":[<clipId>,…],
"bytes":1029496, "nodes":66, "durationSum":78.3 }
},
"clips": { // 46 entries
"sit_bench_swing": {
"group": "sitlean.glb", // which file to fetch
"category": "sitlean",
"duration": 2.1333, // seconds, MEASURED from the keyframe times
"loopable": true, // safe to play on repeat
"loopSeamDeg": 0.04, // MEASURED: max per-bone angle, first key vs last key
"source": "Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx",
"mixamo": "Sitting", // the Mixamo catalogue title
"channels": 65, "rotBones": 65,
"pair": "<clipId>", // OPTIONAL — a matched two-person / open-close partner
"loopDemoted": "…" // OPTIONAL — present iff curated-loopable but seam too wide
}
},
"totalBytes": 3498124, "clipCount": 46, "groupCount": 6, "dedupSavedBytes": 1341724
}
```
Consumer notes for D: the clips ride the **same `_canon` + `_rotOnly` path as `walk`/`idle`** — 65
raw tracks in, **64 out** (Hips.quaternion dropped). `loadPedFleet` today takes `r.anims[0]`; a
group file has N, so read them by name. Everything stays behind your gate — I added no loader code
and touched no file under `web/js/`.
## 5. Where the bytes went — three levers, each measured
Baseline, straight out of Blender as 46 separate GLBs: **8.20 MB**.
| lever | → | why it costs nothing real |
|---|---:|---|
| rotation-only tracks | 7.71 MB | `_rotOnly` decodes every translation/scale track and discards it the same frame. Dead bytes in this engine, not fidelity. `--tracks all` restores them |
| accessor dedup by content | 5.98 MB | lossless — **1 341 724 B** of byte-identical keyframe payloads collapsed |
| int16-normalized rotations | **3.34 MB** | glTF-legal; three r175 de-normalises in `GLTFLoader._getArrayFromAccessor` (checked in `web/vendor/`). **Worst angular error across all 46 clips: 0.0034°** |
**The lever I did NOT pull, for D:** finger channels are **49.7% of the payload** (measured — 3.37 MB
of the pre-quantisation total). `python3 pipeline/clips_pack.py --drop-fingers` takes the whole set
to **1.71 MB**. I kept fidelity; say the word and it is one flag and one minute.
## 6. Why there is NO retarget — and why that is right
The Mixamo bank and PROCITY's ped fleet are **the same mixamorig skeleton**. That is the premise of
the house one-canonical-clip-bank law (`rigs.js` `_canon`). Running mixamorig→mixamorig through
MIRPAMO's retargeter would (a) add transfer error for nothing and (b) **emit the ped MESH into the
clip file**, killing the zero-draw property that makes this round affordable. MIRPAMO's retargeter
is for foreign skeletons (its CMU bonemap) and for rigging raw meshes — it was still run (`make
smoke`, green) as the tool check. The Blender-5 slotted-action gotcha is avoided **by construction**:
nothing goes out through FBX at any stage — FBX in, glTF out.
## 7. The clips, by category (46) — `clipId | Mixamo title | duration | loopable | seam° | source FBX`
### idles.glb — idle (10) — the clone-army killer
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|---|---|---:|:---:|---:|---|
| `idle_breathing` | Breathing Idle | 9.97s | yes | 0.0 | `Breathing_Idle__c9c6d0d5.fbx` |
| `idle_standing` | Standing Idle | 6.03s | yes | 0.0 | `Standing_Idle__c9c831cf.fbx` |
| `idle_look_around` | Looking Around | 6.37s | yes | 0.0 | `Idle_Stand_Looking_Around__c9c6cdf7.fbx` |
| `idle_weight_shift` | Weight Shift | 9.47s | yes | 0.0 | `Shifting_Weight_From_Side_To_Side__c9c908f7.fbx` |
| `idle_cocky_lean` | Being Cocky | 2.93s | no | 0.0 | `Cocky_Lean_Back__c9c90e3c.fbx` |
| `idle_yawn` | Yawn | 8.37s | no | 0.0 | `Big_Yawn_While_Standing__c9c98361.fbx` |
| `idle_happy_1` | Happy Idle | 2.03s | yes | 0.3 | `Happy_Idle_Variation_1__c9ccf37e.fbx` |
| `idle_happy_2` | Happy Idle | 2.97s | yes | 0.0 | `Happy_Idle_Variation_2__c9ccf445.fbx` |
| `idle_old_man` | Old Man Idle | 12.20s | yes | 0.0 | `Old_Man_Standing_Idle__c9c6c004.fbx` |
| `idle_smoking` | Smoking | 17.93s | yes | 0.4 | `Idle_Smoking__c9c603fd.fbx` |
### browse.glb — browse (8)
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|---|---|---:|:---:|---:|---|
| `browse_hold_idle` | Holding Idle | 6.03s | yes | 0.1 | `Holding_An_Object_Idle__c9cda11a.fbx` |
| `browse_hold_walk` | Holding Walk | 1.40s | yes | 6.3 | `Holding_An_Object_While_Walking__c9cda1e7.fbx` |
| `browse_hold_turn_l` | Holding Turn Left | 1.23s | no | 172.5 | `Holding_An_Object_While_Turning_Left__c9cda2af.fbx` |
| `browse_hold_turn_r` | Holding Turn Right | 1.33s | no | 147.3 | `Holding_An_Object_While_Turning_Right__c9cda378.fbx` |
| `browse_open_cabinet` | Opening | 4.83s | no | 0.0 | `Opening_Cabinet_Door__c9c6d49e.fbx` |
| `browse_close_cabinet` | Closing | 6.20s | no | 0.0 | `Closing_Cabinet_Door__c9c6d55d.fbx` |
| `browse_carry_box` | Box Idle | 6.33s | yes | 0.0 | `Carrying_A_Box__c9cd8d90.fbx` |
| `browse_pick_up` | Picking Up Object | 3.47s | no | 92.2 | `Picking_Up_An_Object_With_One_Hand__c9c6c5eb.fbx` |
### sitlean.glb — sitlean (8)
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|---|---|---:|:---:|---:|---|
| `sit_bench_swing` | Sitting | 2.13s | yes | 0.0 | `Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx` |
| `sit_hands_thighs` | Sitting Idle | 6.43s | yes | 0.1 | `Sitting_In_Chair_Hands_Resting_On_Thighs__c9cb9783.fbx` |
| `sit_impatient` | Sitting | 7.03s | yes | 0.0 | `Sitting_And_Impatiently_Waiting__c9ccad56.fbx` |
| `sit_fidget_feet` | Sitting | 1.20s | yes | 0.2 | `Sit_With_Fidgeting_Feet__c9c699a5.fbx` |
| `lean_wall_postures` | One Shoulder Lean | 2.03s | yes | 0.0 | `Leaning_Against_A_Wall_With_Various_Postures__c9c695f0.fbx` |
| `lean_wall_leg_up` | Leaning | 3.37s | yes | 0.0 | `Leaning_Square_Against_A_Wall_With_One_Leg_Up__c9c696b0.fbx` |
| `lean_wall_shoulders` | Leaning | 3.37s | yes | 0.0 | `Leaning_Square_With_Shoulders_Against_A_Wall__c9c69770.fbx` |
| `lean_wall_walkman` | Leaning On A Wall | 1.97s | yes | 0.0 | `Little_Girl_Leaning_On_A_Wall_Listening_To_Music__c9c8bace.fbx` |
### social.glb — social (8)
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|---|---|---:|:---:|---:|---|
| `social_conversation` | Talking | 10.30s | yes | 0.3 | `General_Conversation__c9c88301.fbx` |
| `social_chat_water` | Talking At Watercooler | 44.60s | yes | 0.8 | `Having_A_Chat_At_The_Watercooler__c9c9f1c9.fbx` |
| `social_greeting` | Standing Greeting | 5.13s | no | 0.0 | `Greeting_While_Standing__c9c7996a.fbx` |
| `social_shake_1` | Shaking Hands 1 | 4.40s | no | 0.0 | `2_People_Shaking_Hands_Part_1_-_Female__c9c9f343.fbx` |
| `social_shake_2` | Shaking Hands 2 | 4.40s | no | 0.1 | `2_People_Shaking_Hands_Part_2_-_Male__c9c9f403.fbx` |
| `social_nod_yes` | Head Nod Yes | 2.63s | no | 0.0 | `Nodding_Head_Yes__c9c90d7b.fbx` |
| `social_shake_no` | Shaking Head No | 1.83s | no | 0.0 | `Shaking_Head_No_Dismissively__c9c90b3c.fbx` |
| `social_wave` | Waving | 0.57s | no | 0.0 | `Waving__c9c5ed32.fbx` |
### locomotion.glb — locomotion (6)
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|---|---|---:|:---:|---:|---|
| `walk_shopping_bag` | Walking With Shopping Bag | 1.30s | yes | 0.0 | `Male_Walking_With_Shopping_Bag__c9c64027.fbx` |
| `turn_left_90` | Unarmed Turn Left 90 | 1.53s | no | 84.6 | `Turning_90_Degrees_Left__c9ceef5f.fbx` |
| `turn_right_90` | Unarmed Turn Right 90 | 1.53s | no | 83.2 | `Turning_90_Degrees_Right__c9cef01d.fbx` |
| `turn_walk_180` | Walking Turn 180 | 1.03s | no | 180.0 | `Turning_180_Degrees_While_Walking__c9c9c73f.fbx` |
| `turn_in_place` | Turning | 3.70s | no | 36.7 | `Turning_In_Place__c9c70ef0.fbx` |
| `walk_to_stand` | Stop Walking | 3.03s | no | 55.3 | `Walking_To_Standing_Idle__c9c8d966.fbx` |
### venue.glb — venue (6)
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|---|---|---:|:---:|---:|---|
| `venue_clap_stand` | Clapping | 1.20s | yes | 0.1 | `Clap_While_Standing__c9c60ef2.fbx` |
| `venue_clap_seated` | Clapping | 1.20s | yes | 0.1 | `Clap_While_Seated__c9c60fce.fbx` |
| `venue_cheer` | Cheering | 2.93s | yes | 0.0 | `Male_Cheering_With_Two_Fists_Pump__c9c66dfd.fbx` |
| `venue_northern_soul` | Northern Soul Dance | 16.10s | yes | 0.4 | `Basic_Northern_Soul_Step__c9cbe56d.fbx` |
| `venue_bartending` | Bartending | 13.60s | yes | 0.0 | `Pouring_And_Serving_Drinks__c9c78799.fbx` |
| `venue_headphones` | Listening To Music | 4.50s | yes | 0.0 | `Male_With_Headphones_Listening_To_Music__c9c63329.fbx` |
## 8. Curation — three substitutions, because the named seed was wrong for this town
Every §41.1 seed was checked against `index.json` by name. Three resolved to something the brief
did not intend:
| §41.1 named | what it actually is in the bank | shipped instead |
|---|---|---|
| the whole `45/90/135/180_Degree_{L,R}_Turn` set | **rifle-aiming.** Every single one is Mixamo "Rifle Turn" or "Rifle Crouch Turn". A shopping town does not want citizens sweeping the street down a barrel | the civilian set: `Turning_90_Degrees_Left/Right` (Mixamo **"Unarmed Turn 90"**), `Turning_180_Degrees_While_Walking`, `Turning_In_Place`, `Walking_To_Standing_Idle` |
| `Examine_Forwards` / `_Backwards` | **static poses, not motion** — 262 KB single-frame entries titled "Female Crouch Pose" / "Female Action Pose" | `Opening_Cabinet_Door` + `Picking_Up_An_Object_With_One_Hand` |
| `Legs_Crossed_Leaning_On_Object` | static pose, "Male Standing Pose", 262 KB | `Leaning_Square_Against_A_Wall_With_One_Leg_Up` + `…_With_Shoulders_Against_A_Wall` |
`2_People_Shaking_Hands_Part_1/2` **does** exist (`…_-_Female` / `…_-_Male`) and **ships as a pair**,
flagged `"pair"` in the manifest — as do the two holding-turns and the cabinet open/close.
Two picks are deliberately for *this* game rather than generic: `Idle_Smoking` and
`Male_Walking_With_Shopping_Bag` (1990s Australian high street), plus `Pouring_And_Serving_Drinks`
for the pub and `Male_With_Headphones_Listening_To_Music` for the record shop.
## 9. Verification — every claim above is a measurement
**`pipeline/clips_verify.py` → GREEN, 0 errors, 0 warnings** (new, re-runnable; **F may gate on it**
— it is the "every referenced clip resolves" check). It asserts: file sizes match the manifest ·
every clip id resolves to a named animation and nothing extra rides along · every group is zero-draw
and Draco-free · all six carry the identical 66-node skeleton · every channel targets a real node ·
every declared duration equals the keyframe times in the bytes · every clip survives `_rotOnly` with
tracks left · quantisation error is bounded.
**In the repo's own vendored three.js r175**, through `GLTFLoader` + `SkeletonUtils.clone` of a real
`man_casual_01.glb`, all 46 clips: **65 raw tracks → 64 after `_rotOnly`**, durations match to 3
decimals, every sampled bone world-position finite and moving, 0 meshes / 0 tris added.
**The R16 trap was checked for on purpose.** R16 found that keeping `Hips.quaternion` on a seated
clip lays the body flat (head folded to hip height). Spine tilt (Hips→Head vs +Y) sampled at 24
points across every clip: **0 frames over 75°, on all 46.** Worst is 36.5° (`social_chat_water` —
a lean on the watercooler); the seated family sits at 2728°, a natural seated forward lean.
**Nothing folds.**
**Loopability is measured.** `loopSeamDeg` = max per-bone angle between first and last rotation key.
28 of 46 loopable. One curated-loopable clip was **auto-demoted by the packer**`turn_in_place`,
36.7° seam — and carries `loopDemoted` saying so. The one-shots read exactly as expected
(`browse_hold_turn_l` 172.5°, `turn_left_90` 180°): they are turns, they are not supposed to close.
## 10. Re-running it (all $0, all on-device, ~2 minutes)
Added to `pipeline/`: `clips_r41.json` (the checked-in selection — **every asset by name, no
wildcard, ruling 3**), `clips_export.py` (Blender), `clips_pack.py` (pure stdlib), `clips_verify.py`.
```bash
# 1. fetch the 46 named FBX from ultra's GREEN Mixamo bank (BY NAME — never a wildcard)
python3 - <<'EOF' > /tmp/fbx_list.txt
import json;print('\n'.join(c['file'] for g in json.load(open('pipeline/clips_r41.json'))['groups'].values() for c in g['clips']))
EOF
ssh johnking@100.91.239.7 "cd ~/Documents/MOTIONLIB/green/mixamo_full && tar czf - $(tr '\n' ' ' </tmp/fbx_list.txt)" | tar xzf - -C /tmp/fbx
# 2. FBX -> per-clip skeleton-only GLB (local Blender 5.1.2; FBX in, glTF out — never FBX out)
/Applications/Blender.app/Contents/MacOS/Blender --background --python pipeline/clips_export.py -- \
--list pipeline/clips_r41.json --fbx /tmp/fbx --out /tmp/clip_glb
# 3. merge into the <=6 grouped GLBs + write the manifest
python3 pipeline/clips_pack.py --stage /tmp/clip_glb --out web/models/clips \
--manifest web/assets/motion_manifest.json # [--drop-fingers] [--no-quantize] [--tracks all]
# 4. gate
python3 pipeline/clips_verify.py --stage /tmp/clip_glb # 0 errors = green
python3 pipeline/glb_stat.py --json web/models/clips # tris must be 0 on all six
```
## 11. Handoff
**Lane D (41.3) — you are unblocked.** `web/assets/motion_manifest.json` is the contract; §4 above is
the schema. `web/models/clips/*.glb` are plain uncompressed GLBs, no Draco, no decoder. Nothing in
`web/js/**` was touched — no loader code, no gate, no default changed, so `?noassets=1` and
`?classic=1` are byte-identical to R40 until you wire it. The per-group `bytes` are in the manifest
so you can pick what boots: `sitlean` + `locomotion` together are **571 KB**; the whole set is
**3.34 MB in 6 fetches**. If that is too much, `--drop-fingers` halves it in one minute.
**Lane F (41.6):** `python3 pipeline/clips_verify.py` is a ready-made clip-manifest-integrity gate —
exit 0 = green, and it fails loudly on a missing file, a renamed clip, a size drift, a non-zero-draw
group or a skeleton mismatch. `pipeline/glb_stat.py --json web/models/clips` is the zero-draw proof.
**Files I touched, and only these:** `web/models/clips/{idles,browse,sitlean,social,locomotion,venue}.glb`
(new) · `web/assets/motion_manifest.json` (new) · `pipeline/{clips_r41.json,clips_export.py,clips_pack.py,clips_verify.py}`
(new) · `pipeline/AUDIT.md` · `pipeline/README.md` · `E-progress.md` · `docs/LANES/LANE_E_NOTES.md`.
Outside the repo, for ruling 1 only: `~/Documents/MIRPAMO/library/**`. **No git command was run.**
I did not touch `web/assets/manifest.json` (props half's), `web/js/**`, or `tools/qa.sh`.