# 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 3–16 materials on a single mesh** (one flat colour per record sleeve, `Mat_Sleeve_`), 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 5–31 materials, so "one mesh" was still 5–31 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, 2–5 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.85–0.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 2–5 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/`) — 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 (30–96k, 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** (31k–56k each) — the gate-3 blocker. Decimated to **≤3k each** (1564–2805; 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 ` 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:` 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 25–77k 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:` 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 `@.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[]` once, then index `clips[]` — 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":[,…], "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": "", // 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 27–28°, 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' ' ' 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`.