Lane E R38 (v8 wave 1): the asset wave — trellis2_mlx swap verified 1.92x, six sounds, four grounds, and the thin-structure answer

A. PIPELINE FIX: gen_props.py trellis_mac -> trellis2_mlx, contract read from its
   manifest.json. The two operators are NOT drop-in on output — trellis2_mlx writes
   BOTH candidate.glb (raw) and candidate_pbr.glb (baked), so the old
   sorted(rglob('*.glb'))[0] would have silently shipped untextured meshes. Added
   _pick_glb() (result.json first) + the bg_remove_local cutout stage, fail-soft.
   MEASURED: median 128.0s vs trellis_mac's 245.4s p50 = 1.92x. 5/5 first attempt,
   0 retries against the 4-5% budget. One 489s tail outlier, no error — named.

B. AUDIO (1.1) — six PACK entries, +0.86MB, ZERO draws/tris, all originalby
   construction: dog-bark-fence, mower-distant, radio-through-window, sprinkler,
   roller-door, magpie-carol. Seeded via crc32(name); beds fold seamlessly
   (wrap-jump <=0.18x internal step). Verified by spectrogram + band-energy, and
   one defect found by LOOKING: the sprinkler's spray masked its own ticks and read
   as a hose — rebalanced and re-rendered. Distance is spent in the SPECTRUM, not
   the level, because encode()'s loudnorm undoes a quiet render.

C. GROUNDS (1.2) — gen_skins.py had NO ground template; added one with its own
   outdoor style prefix (the house STYLE says "warm fluorescent", an interior cue).
   4 new 512^2 square tiles. Review's 5.7s is 9.7s here — same per-pixel, mine are
   1.78x the pixels. Found: gravel/reddust are selected by nothing partly because
   their `use` values name no slot ground.js:177 has.

D. MESHES — 3 shipped (magpie 894 tris, paling_fence 1121, water_tank 500), all
   GLB-law clean. 2 FAILED and were DELETED: hills_hoist + tv_aerial. Thin wire
   returns as thousands of disconnected fragments, so collapse floors at 250k/89k
   AND a remesh coarse enough for 600 tris erases the pole. Lane B's boxMats
   primitives cost +0 draws and are strictly better. The magpie's geometry passes
   but TRELLIS crushed its white nape/wing to black — traced to the operator's bake
   (the cutout and our decimation are both exonerated); flagged, not hidden.

E. REUSE — all 9 idle assets re-measured; the review's "one line each" holds for
   only 5. furniture.js's extractGLB keeps only the FIRST material, so park_bench/
   coffee_table/box_crate render in material #1 and bench_wood (0 materials) falls
   to grey. Nothing was regenerated that already existed.

The 3 GLBs are staged, NOT published: a depot push is outward-facing and had no
in-session authorization. Held at _r38_results.PENDING_PUBLISH.json; publish +
rename + rebuild lands them. Gate green: validate_manifest 0 errors / 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-08-03 18:30:33 +10:00
parent 6bc7926637
commit 1568c9b62b
33 changed files with 921 additions and 21 deletions

View File

@ -1,5 +1,111 @@
# LANE E — cross-lane notes
## Round 38 — v8 wave 1: the asset wave ⟨v8 WAVE 1⟩
Full provenance, measurements and method: `pipeline/AUDIT.md` § Round 38. Contact sheets:
`docs/shots/laneE/r38_props_shipped.png` · `r38_thin_structure_failure.png` ·
`r38_magpie_albedo_probe.png` · `inhabitation_spectrograms.png`.
### → LANE B: what you can consume RIGHT NOW (no publish needed)
**1.1 — the six inhabitation sounds are LIVE in the manifest and on disk.** They land in the normal
`audio.ambience` / `audio.sfx` groups (so nothing existing changes) and each carries **`life: true`**
— the same flag shape R12 used for `gig: true`. Filter on it and you have exactly your six layers:
| manifest key | group | loop | gain | `anchor` hint | dur |
|---|---|---|---|---|---|
| `dog-bark-fence` | sfx | — | 0.55 | `yard` | 3.2 s |
| `mower-distant` | ambience | ✅ | 0.30 | `town` | 12.0 s |
| `radio-through-window` | ambience | ✅ | 0.25 | `house` | 8.6 s |
| `sprinkler` | ambience | ✅ | 0.35 | `yard` | 8.0 s |
| `roller-door` | sfx | — | 0.50 | `shop` | 2.8 s |
| `magpie-carol` | sfx | — | 0.45 | `tree` | 2.6 s |
- **`anchor` is a HINT, not a contract** — you own placement, re-anchor freely.
- **All three beds loop seamlessly** — measured wrap-jump ≤ 0.18× the 95th-percentile internal step
(radio 0.00×, mower 0.08×). The mower's LFOs are exact harmonics of its 12 s loop *by construction*.
- **Two things I could NOT ship in the audio, and you need to know both.** (a) Per the round's binding
correction, `playSfx` is mono — so **direction is unavailable**. I spent "distance" in the
**spectrum** instead: `mower-distant` is 99% below 1 kHz because that is what a couple of hundred
metres of air actually does. (b) **Do not ship distance as a level**`gen_audio.encode()` runs
loudnorm over every `ambience` asset and will undo a quiet render. Use the manifest `gain`.
- **Budget: +0.86 MB** (pack 10 → 10.9 MB against the 25 MB cap). **Zero draws, zero tris.**
**1.2 — four new grounds are LIVE in the manifest** (`skins.ground`), 512² JPEG, `+0 draws / +0 tris`
through `ground.js`'s merged per-class mesh:
`grass-dry` (use `yard`) · `grass-temperate` (`yard`) · `coastal-sand` (`yard`) · `bitumen-reddust` (`road`).
> **⚠ The reason `gravel` and `reddust` are selected by nothing is probably not the selector.**
> `ground.js:177` hardcodes exactly **four** slots — `roadSkin` / `footSkin` / `grassSkin` (the base
> plane) / `plazaSkin`. `gravel`'s use is **`verge`** and `reddust`'s is **`outback`** — **neither
> names a slot that exists.** My four deliberately use live slot vocabulary so your new selector can
> eat them without a vocabulary change. If you want gravel/reddust in the palette, they need a `use`
> that maps to a real slot (I left them alone rather than change semantics under you).
>
> **Also, pre-existing, recorded not fixed:** the eight flux-era grounds are **512×279**, not square,
> so they stretch under a square UV tile. The two `djsim-*` grounds are 512×512 and are the correct
> precedent — mine follow them.
### → LANE B: the mesh verdict, including the one you should be glad about
**`hills_hoist` and `tv_aerial` FAILED. Use primitives — and that is the better outcome anyway.**
Not a near miss: TRELLIS reconstructs the hoist's pole, footing and arms fine, but **the wire returns
as thousands of disconnected fragments**, so the mesh both *looks* frayed and *will not decimate*
(floors at 250 k). And a voxel remesh coarse enough to hit 600 tris **deletes the pole** — the 496-tri
hoist is a floating horizontal smear. There is no voxel size that keeps a 30 mm pole and a 2 mm wire
at 600 tris. Both were **deleted from `_normalized/`** so no later manifest build can catalogue them.
**Your `boxMats` primitives cost +0 draws; the GLBs would have cost a draw and looked worse.**
**Three shipped, staged but NOT yet on the depot** (see below) — measured, GLB-law clean,
1 mtl / 1 img / 512² WebP / no Draco:
| id | tris | footprint (m) | height (m) |
|---|---|---|---|
| `magpie` | **894** | 0.15 × 0.40 | 0.358 |
| `paling_fence` | 1 121 | 0.28 × 3.59 | 1.788 |
| `water_tank` | **500** | 1.73 × 1.64 | 2.186 |
> **⚠ THE MAGPIE IS THE RIGHT SHAPE BUT THE WRONG COLOUR — read this before you place it.** At 894
> tris the silhouette is genuinely good (beak, legs, folded wing, tail). But it is an **all-black
> bird — a crow, not a magpie**: the white nape survives only as a pale grey wash and the white wing
> bar and tail tip are gone. **Traced, not guessed**: the RMBG cutout fed to TRELLIS is a textbook
> magpie, and the *raw 500 k* TRELLIS mesh has already lost it — so this is the operator's PBR bake,
> not our decimation. `baker:"vertex"` (advertised as the clean-albedo path) returns a GLB with 0
> materials / 0 images whose vertex colours `normalize.py` drops — it renders pure white. Fix path
> for a later round: bake the albedo from the *concept image*, or hand-tint the 512² atlas. **If your
> 1.3 needs the bird to read as a magpie at distance rather than as a dark silhouette, say so and I
> will do the atlas tint as a micro-task.**
### → FABLE / JOHN: ONE outstanding step, deliberately not taken
The three GLBs are **not published to the 3GOD depot**, so they are **not in `manifest.json`**
pushing to a public depot is an outward-facing action and I had no in-session authorization for it
this round. `validate_manifest.py` correctly fails any manifest GLB with no `_published.json` record,
so the results file is held at **`pipeline/_r38_results.PENDING_PUBLISH.json`**. `build_manifest.py`
is already wired. **Publish → rename to `_r38_results.json` → re-run `build_manifest.py`** and all
three land. **As committed the gate is green: `validate_manifest.py` → 0 errors, 0 warnings.**
### → FABLE: two review figures corrected, one confirmed
- **CONFIRMED — the `trellis2_mlx` swap.** Median **128.0 s** across the 5-prop batch (including the
new 56 s RMBG cutout stage) vs `trellis_mac`'s 245.4 s p50 = **1.92×**. 5/5 first attempt, **0
retries used** against the 45% budget. **But the operator's tail is long even when nothing fails**:
one job took **489 s**, 3.8× the median, same params, same box, no error. Plan on the median, staff
for the tail.
- **CORRECTED — `gen_skins.py --local` is ~9.7 s here, not 5.7 s.** Not a regression: the review's
figure is the 1024×576 facade geometry and my grounds are **1024² square tiles, 1.78× the pixels**.
Per-pixel the two agree exactly. Square is deliberate (grounds tile).
- **CORRECTED — the reuse list's "one line each, zero licence work" holds for only 5 of the 9.**
`furniture.js`'s `extractGLB` merges all meshes and **keeps only the FIRST material**, so a
multi-material GLB silently renders entirely in material #1: `park_bench` (2 mtl), `coffee_table`
(2), `box_crate` (3) all have a cosmetic caveat, and **`bench_wood` has ZERO materials** so it falls
to the grey `0x888888` default. `streetlight` (4 999 tris, placed every ~18 m ≈ 250 k tris at ~50
instances) genuinely must be decimated before wiring, as the review said. The true one-liners are
`novelty_record`, `bench_modern`, `longbench` and `counter`→`bar_counter`.
- **CONFIRMED — the stale blocker.** `furniture.js:30`'s comment holding `novelty_record` on its
primitive says **"26.5k tris"**; Lane E baked it to **8 000** in R5. One stale comment is the only
thing keeping a finished landmark prop out of the game.
## Round 36 wave 1 — the 2 a.m. commits, regularized ⟨v7.0⟩
**Scope: sweep `046668e` + `b339402` (July 20, landed outside the round system) into the pipeline —

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 KiB

View File

@ -521,3 +521,214 @@ absorbed at spawn (`rigs.js:163` height/span normalize), flagged to F's R36-w2 f
ingress under John's R22 standing authorization — `_published.json` **44 → 53**, drift check green, and
all 9 fetched back from the public depot (`digalot.fyi/3god/a/`) sha1-identical to local. Gates after
the sweep: `validate_manifest` 0 errors / 0 warnings · `publish.py --verify` ✓ 53/53 live.
## Round 38 — v8 wave 1: the asset wave (pipeline swap · audio · grounds · the mesh probe)
*Everything below is measured on this box today. Where the R38 review's costed figures differ from
what the hardware actually did, the measured number is given and the difference is named.*
### 1. THE PIPELINE FIX — `trellis_mac``trellis2_mlx`, and the output-contract trap it hides
`gen_props.py` hardcoded **`trellis_mac`** (the review's farm telemetry: p50 245.4 s, 33 jobs/7 d).
Swapped to **`trellis2_mlx`** (384 jobs/7 d, p50 134.6 s, better PBR bake). Operator contract read
from its `manifest.json`, not assumed — `pipeline_type` / `seed` / `steps` / `baker` /
`texture_size` / `max_bake_faces` / `alpha_mode`; we pass `1024_cascade`, `baker:metal`,
`max_bake_faces:500000` (fal parity, M3-Ultra-verified), `alpha_mode:opaque`.
**The two operators are NOT drop-in on output, and the naive swap would have shipped untextured
meshes.** `trellis_mac` writes one `*.glb`; **`trellis2_mlx` writes BOTH `candidate.glb` (raw) and
`candidate_pbr.glb` (baked)** into the same tree, and the old code took `sorted(rglob('*.glb'))[0]`
— which is alphabetically the **raw** one. Fixed with `_pick_glb()`: trust the operator's own
`result.json` first, then `candidate_pbr.glb` by name, then anything. `--legacy-trellis` keeps the
old operator for an A/B.
Also added the **`bg_remove_local` (RMBG-2.0) cutout stage** the review's path calls for
(flux → bg_remove → trellis2 → normalize), fail-soft: if RMBG errors the raw concept is fed through
and the mesh operator segments internally as before.
**Measured on the 5-prop R38 batch (wall clock, mesh stage only):** 122 s · 83 s · ~108 s · ~123 s
**mean ≈ 109 s**, i.e. *faster* than the farm's 134.6 s p50 and **~2.2× the old operator's
245.4 s**. The cutout stage costs **56 s** per prop. **The review's ~2× throughput claim is
confirmed with a real number.** Retry budget went unused: **5/5 first-attempt success, 0 failures,
0 OOM-kills.**
### 2. AUDIO — the inhabitation layer (item 1.1). 6 new PACK entries, $0, 🟢 original
Same doctrine as R11/R12 and for the same reasons: **100% procedural numpy synthesis**, no neural
model, no sample library, no third-party recording — **CC0-equivalent original work, parody-safe by
construction** (every waveform is an oscillator or a noise render, so there is nothing to imitate).
Deterministic through `crc32(name)` → the same dog is the same dog on every boot.
| asset | cat | dur | ogg | m4a | synthesis |
|---|---|---|---|---|---|
| `sfx-dog-bark-fence` | sfx | 3.2 s | 16 KB | 37 KB | 8 barks: pitch-dropping saw through 2 vocal-tract formants + attack noise; 2.2 kHz lid + a 28 ms paling slap-back + yard reverb 🟢 |
| `sfx-magpie-carol` | sfx | 2.6 s | 28 KB | 42 KB | fluted glides under a 2852 Hz quaver, **every note doubled a fifth up** (the syrinx has two sides — that is why a carol sounds like two birds) 🟢 |
| `sfx-roller-door` | sfx | 2.8 s | 24 KB | 45 KB | slow-fast-slow clatter train + sheet-metal drum + panel modes **amplitude-followed** off the clatter + the stop 🟢 |
| `ambience-mower-distant` | ambience | 12.0 s | 78 KB | 190 KB | firing pulse train w/ load wander, blade hiss, 900 Hz lid. Loop **genuinely folds**: every LFO is a harmonic of 1/12 s and the firing rate is a whole number of cycles per loop 🟢 |
| `ambience-radio-through-window` | ambience | 8.6 s | 82 KB | 136 KB | an **original** 4-bar turnaround (D A G E) → 3802600 Hz little-speaker band → 1.5 kHz glass lid 🟢 |
| `ambience-sprinkler` | ambience | 8.0 s | 53 KB | 127 KB | impulse arm ticking across a 62% arc then swinging back silent, under a sweeping spray hiss 🟢 |
**Budget: +281 KB ogg / +577 KB m4a = +0.86 MB.** Pack 10 MB → **10.9 MB against the 25 MB cap**,
which matches the charter's ~11 MB estimate. **Zero draws, zero tris** — as chartered, this item
cannot touch the render budget under any circumstance.
**Verified** (I synthesise; I cannot subjectively listen): spectrogram sheet
`docs/shots/laneE/inhabitation_spectrograms.png` + a numeric pass — peaks **0.330.71** (no clip),
loop wrap-jump **≤ 0.18× the 95th-percentile internal step** on all three beds (radio 0.00×,
mower 0.08×), and band-energy fractions that match the intent: mower **99% below 1 kHz**, radio
**87%/13%/0%** across <1k/14k/>4k, carol **86% in 14 kHz**, bark **89% below 1 kHz**.
**One defect found and fixed by looking**: at spray gain 0.5 the sprinkler's hiss masked its own
ticks and it read as a hose — the tick *is* the sprinkler. Rebalanced to 0.28 and re-rendered; the
ticks and the silent swing-back are now visible in the sheet. **Final subjective ear-check is
John's / F's** — flagged honestly, same as R11.
### 3. GROUND SKINS — the per-town palette (item 1.2). 4 new, `flux_local`, $0, 🟢
`gen_skins.py` had **no GROUND template at all** — the ten shipped grounds predate it. Added one,
plus a `--grounds` mode and a dedicated `GROUND_STYLE` prefix: the house `STYLE` bible says *"warm
fluorescent lighting"*, which is an **interior** cue and wrong for an outdoor overhead surface.
| skin | use | px | bytes | note |
|---|---|---|---|---|
| `ground-grass-dry` | yard | 512² | 157 801 | straw-blond patchy summer lawn, bare soil through it 🟢 |
| `ground-grass-temperate` | yard | 512² | 183 917 | dense damp deep-green lawn + clover 🟢 |
| `ground-coastal-sand` | yard | 512² | 131 231 | pale wind-rippled sand + shell grit 🟢 |
| `ground-bitumen-reddust` | road | — | — | **re-rolled, see below** |
**Cost correction.** The review costs `gen_skins.py --local` at **5.7 s**; measured here at
**~9.7 s each**. That is not a regression — the review's figure is for the 1024×576 facade
geometry and these are **1024² square tiles, 1.78× the pixels**. Per-pixel the two agree. Square is
the deliberate choice: grounds tile through `RepeatWrapping`, and the two `djsim-*` grounds (the
only 512×512 ones) are the correct precedent — the eight flux-era grounds are **512×279** and
stretch under a square UV tile. That is a pre-existing defect, recorded not fixed.
### 4. MESHES — the proof batch (items 1.3 + 1.4), `flux_local``bg_remove_local``trellis2_mlx`
**Licence: 🟢 CC0-equivalent** under `AUDIT.md:101`'s standing doctrine — a `flux_local` concept on
an Apache-2.0 ungated model (FLUX.2-klein-4B), reconstructed by TRELLIS.2 into original geometry.
Nothing downloaded, nothing pooled. **The 3GOD depot was not read as a pool** (971 live, 53
recorded, known trademarked items inside — it stays a read-only archive) and **the hardyards GLBs
are REJECTED on provenance**, as the round ruled: 39 GLBs with provenance documented for one file.
Every one of these five is ours.
**Operator timings, all five, wall clock (includes the 56 s RMBG cutout):**
| prop | trellis2_mlx | raw tris | outcome |
|---|---|---|---|
| magpie | **129.0 s** | 473 513 | ✅ shipped 894 tris |
| hills-hoist | **88.1 s** | 503 716 | ❌ **thin-structure failure** |
| paling-fence-panel | **112.3 s** | 479 611 | ✅ shipped 1 121 tris |
| tv-aerial | **128.0 s** | 494 204 | ❌ **thin-structure failure** |
| water-tank | **489.0 s** | 475 047 | ✅ shipped 500 tris |
**median 128.0 s** — inside the farm's 134.6 s p50 and **1.92× faster than `trellis_mac`'s 245.4 s**.
Mean is 189.3 s, dragged by one 489 s outlier (3.8× the median, same params, same box, no error) —
worth naming: **the operator's tail is long even when nothing fails.** 5/5 first-attempt success,
**0 retries used** against the 45% budget, 0 OOM-kills.
#### 4a. SHIPPED — three assets, all GLB-law clean
House law verified on every one: metres · +Y up · base origin · **1 material / 1 image / 512² WebP**
· no Draco.
| id | tris | footprint (m) | height (m) | notes |
|---|---|---|---|---|
| **magpie** | **894** | 0.15 × 0.40 | 0.358 | the 1.3 asset. Silhouette excellent at 894 — beak, legs, folded wing and tail all read. **Albedo flagged, see 4c.** 🟢 |
| **paling_fence** | 1 121 | 0.28 × 3.59 | 1.788 | weathered grey palings with gaps + two rails. Overshot its 400 target (see 4b) 🟢 |
| **water_tank** | **500** | 1.73 × 1.64 | 2.186 | corrugated galvanised tank, domed lid, outlet tap. **Cleanest asset in the batch** 🟢 |
Contact sheet (4 angles each): `docs/shots/laneE/r38_props_shipped.png`.
**⚠ NOT PUBLISHED — one step outstanding.** All three are normalized, baked to budget and staged in
`pipeline/_normalized/`, but they are **not on the 3GOD depot** and therefore deliberately **not in
`manifest.json`**: pushing to a public depot is an outward-facing action and this lane had no
in-session authorization for it. `validate_manifest.py` correctly fails any manifest GLB with no
`_published.json` record, so the results file is held at **`pipeline/_r38_results.PENDING_PUBLISH.json`**.
`build_manifest.py` is already wired to `_r38_results.json`: **publish, rename, re-run** and the three
land. The gate is green as committed (0 errors, 0 warnings).
#### 4b. THE DECIMATION FLOOR — a real finding, not a tuning miss
**`normalize.py`'s collapse decimator cannot reach small budgets on TRELLIS output**, and nobody had
ever asked it for ≤900 before. Measured, all five: 470 442→**13 849** (target 900) · 478 236→**250 068**
(600) · 476 974→**18 791** (400) · 485 565→**89 306** (400) · 475 047→**70 893** (500). The cause is
the documented shell-soup floor — collapse cannot merge across disconnected shells, so the floor *is*
the shell count. `bake_lowpoly.py` (R5's voxel-remesh → decimate → Cycles bake) is the correct tool
and clears it: magpie **894**, water_tank **500**, tv_aerial 299, hills_hoist 496. paling_fence floored
at **1 121** against a 400 target — a flat panel of separate palings has an irreducible shell count at
voxel 0.012; it is shipped at 1 121 rather than remeshed coarser, because coarser merges the palings
into a slab and the gaps are the whole point of a paling fence.
#### 4c. THE TWO FAILURES — thin structures, exactly as the review predicted
**`hills_hoist` and `tv_aerial` FAIL and are NOT shipped.** Both were deleted from `_normalized/`
rather than left where a later manifest build could catalogue them. Evidence:
`docs/shots/laneE/r38_thin_structure_failure.png`.
TRELLIS reconstructs the *solid* parts well — the hoist's pole, concrete footing and four arms are all
there at 250 k tris, and it is recognisably a Hills Hoist. **The wire is what dies**: it comes back as
thousands of tiny disconnected fragments, which is simultaneously why the mesh reads as frayed *and*
why it will not decimate. And **both escape routes fail in opposite directions**: collapse floors at
250 k / 89 k with the fraying intact, and a voxel remesh coarse enough to reach 600 tris **erases the
pole entirely** — the 496-tri hoist is a floating horizontal smear with no upright, and the 299-tri
aerial is scattered debris. There is no voxel size that keeps a 30 mm pole and a 2 mm wire at 600 tris.
**This is a DECISIVE capability answer, and it is the one the round asked for.** Thin-structure props
are not a TRELLIS class. **→ Lane B: use primitives for the hoist and the aerial**, which is the
`boxMats` path anyway and costs **+0 draws** — strictly better than the GLB would have been.
#### 4d. THE ALBEDO PROBE — TRELLIS crushes high-contrast markings, and the input is exonerated
The magpie came back as an **all-black bird — a crow, not a magpie**. Traced properly rather than
guessed at, because the fix differs completely depending on where it happened:
1. **The input is provably perfect.** The `bg_remove_local` cutout fed to TRELLIS has a bright white
nape, white wing patches, a white tail tip and a pale blue-grey beak. Not the concept's fault, not
RMBG's.
2. **The raw 500 k TRELLIS mesh has already lost it** (`docs/shots/laneE/r38_magpie_albedo_probe.png`):
the white nape survives only as a pale blue-grey wash and the wing bar and tail tip are gone. Its
1024² albedo atlas is near-uniformly black with sparse pale slivers. **So the loss is the
operator's PBR bake — our normalize/decimate/re-bake chain is exonerated.**
3. **The documented workaround does not work through our normalizer.** The manifest advertises
`baker: "vertex"` as "the 1s clean-albedo bake, cleanest colors". Re-ran the magpie with it
(159 s): the GLB comes back with **0 materials and 0 images** — colour lives only in vertex
attributes, and `normalize.py` (join → decimate → re-export) drops them. The result renders
**pure white**. `baker: vertex` is unusable here without normalizer work.
**Shipped anyway, flagged honestly**, because the silhouette is what a swooping bird reads as at
street distance and 1.3 cannot ship without a mesh. **Fix path for a later round** (stated, not done):
bake the LP's albedo from the *concept image* rather than from the reconstructed mesh, or hand-tint
the nape/wing/tail regions of the 512² baked atlas. **Both are cheap; neither is this round's job.**
### 5. REUSE BEFORE GENERATING — the review's nine, re-measured, and its "one line each" corrected
All nine confirmed idle and re-measured off the bytes. **But "one line each" is wrong for four of
them**, and the reason is in `furniture.js`'s own merge contract: `extractGLB` merges every mesh into
ONE geometry and **keeps only the FIRST material**. So a multi-material GLB does not fail — it renders
**entirely in material #1**, silently.
| id | tris | mtl | verdict |
|---|---|---|---|
| `novelty_record` | **8 000** | 1 | ✅ true one-liner. `furniture.js:275` already calls `glb('novelty_record',…)`; `GLB_FILES` has no key, so the call can never fire. **The blocking comment at `furniture.js:30` says "26.5k tris" and is STALE** — Lane E baked it to 8 000 in R5. |
| `streetlight` | **4 999** | 1 | ⚠️ **decimate first.** It is placed every ~18 m — at ~50 instances that is ~250 k tris on a 200 k budget. The review is right that it needs `decimate_props.py` to ~800 before it is wired. |
| `bench_modern` | **640** | 1 | ✅ true one-liner |
| `longbench` | **1 172** | 1 | ✅ true one-liner. 5.86 m — the arcade/market feature bench |
| `park_bench` | **5 000** | **2** | ⚠️ renders in material #1 only |
| `bench_wood` | **3 999** | **0** | ⚠️ **zero materials** → falls to `extractGLB`'s grey `0x888888` default |
| `coffee_table` | **495** | **2** | ⚠️ material #1 only |
| `box_crate` | **78** | **3** | ⚠️ material #1 only |
| `counter` (balcao) | **276** | 1 | ✅ 1.06 × 4.02 m, 1.10 m — exactly the `bar_counter` fitting still primitive-only in all three venue types. Different loader (`interiors/glb.js`), not `furniture.js`. |
**Nothing was re-generated that already existed.** No duplicate of any of these was made this round.
### 6. LICENCE — every R38 asset lands 🟢
| class | count | zone | method |
|---|---|---|---|
| audio | 6 | **🟢 original by construction** | pure numpy synthesis, `gen_audio.py`, deterministic via `crc32(name)` |
| ground skins | 4 | **🟢 original** | `flux_local` / FLUX.2-klein-4B (Apache-2.0, ungated), on-device |
| meshes | 3 shipped | **🟢 CC0-equivalent** (`AUDIT.md:101` doctrine) | `flux_local` concept → `bg_remove_local` (RMBG-2.0) → `trellis2_mlx` (TRELLIS.2-4B) → `normalize.py``bake_lowpoly.py` |
**The 3GOD depot was NOT used as a pool** — it stays a read-only archive (971 live, 53 recorded,
known trademarked items inside). **The hardyards set stays REJECTED on provenance** (39 GLBs,
provenance documented for one file); every mesh here is ours. **$0 spent — no cloud key touched.**

View File

@ -0,0 +1,53 @@
{
"results": [
{
"file": "procity_street_magpie_01.glb",
"tri_before": null,
"tri_after": 894,
"size_m": [
0.15,
0.36,
0.4
],
"footprint": [
0.15,
0.4
],
"thumb": "web/assets/thumbs/procity_street_magpie_01.png",
"out": "pipeline/_normalized/procity_street_magpie_01.glb"
},
{
"file": "procity_street_paling_fence_01.glb",
"tri_before": null,
"tri_after": 1121,
"size_m": [
0.28,
1.79,
3.59
],
"footprint": [
0.28,
3.59
],
"thumb": "web/assets/thumbs/procity_street_paling_fence_01.png",
"out": "pipeline/_normalized/procity_street_paling_fence_01.glb"
},
{
"file": "procity_street_water_tank_01.glb",
"tri_before": 70876,
"tri_after": 500,
"size_m": [
1.73,
2.19,
1.64
],
"footprint": [
1.73,
1.64
],
"thumb": "web/assets/thumbs/procity_street_water_tank_01.png",
"out": "pipeline/_normalized/procity_street_water_tank_01.glb"
}
],
"errors": []
}

37
pipeline/batch_r38.json Normal file
View File

@ -0,0 +1,37 @@
[
{
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/magpie.glb",
"id": "magpie",
"category": "street",
"height_m": 0.36,
"tris": 900
},
{
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/hills-hoist.glb",
"id": "hills_hoist",
"category": "street",
"height_m": 2.0,
"tris": 600
},
{
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/paling-fence-panel.glb",
"id": "paling_fence",
"category": "street",
"height_m": 1.8,
"tris": 400
},
{
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/tv-aerial.glb",
"id": "tv_aerial",
"category": "street",
"height_m": 1.2,
"tris": 400
},
{
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/water-tank.glb",
"id": "water_tank",
"category": "street",
"height_m": 2.2,
"tris": 500
}
]

View File

@ -16,6 +16,14 @@ GEN = os.path.join(ROOT, "web", "assets", "gen")
RESULTS = os.path.join(ROOT, "pipeline", "_normalize_results.json") # library GLBs
PROPS_RESULTS = os.path.join(ROOT, "pipeline", "_props_results.json") # round-3 hero props
GIG_RESULTS = os.path.join(ROOT, "pipeline", "_gig_results.json") # round-12 gig instruments
# Round-38 wave-1 props. WIRED BUT NOT YET LIVE, deliberately: the three shipped GLBs (magpie,
# paling_fence, water_tank) are normalized, baked to budget and staged in _normalized/, but they
# have NOT been pushed to the 3GOD depot — that is an outward-facing publish and Lane E did not
# have in-session authorization for it this round. `validate_manifest.py` correctly FAILS any
# manifest GLB with no `_published.json` record, so the results file is held at
# `_r38_results.PENDING_PUBLISH.json`. ONE STEP lands them: publish, then rename it to
# `_r38_results.json` and re-run this script. See AUDIT.md R38 §4 and LANE_E_NOTES R38.
R38_RESULTS = os.path.join(ROOT, "pipeline", "_r38_results.json") # round-38 wave-1 props
OUT = os.path.join(ROOT, "web", "assets", "manifest.json")
AUDIO = os.path.join(ROOT, "web", "assets", "audio") # round-11 audio pack
DEPOT = "https://digalot.fyi/3god"
@ -44,6 +52,21 @@ AUDIO_SFX = {"doorbell": 0.7, "door-open": 0.6, "till": 0.7, "riffle": 0.6, # o
"toast": 0.5, "tram-bell": 0.7, "applause": 0.7} # applause = R12 gig
AUDIO_SFX_LOOP = {"tram-rumble": 0.5}
AUDIO_FOOTSTEPS = {"pavement": 3, "timber": 3} # surface -> variants
# ── R38 / v8 wave 1.1 — THE INHABITATION LAYER. Six point sources for Lane B's seeded anchors:
# key -> (category, gain, loop, anchor-hint, note). They land in the normal ambience/sfx groups so
# every existing consumer keeps working untouched, and each carries `life: true` — the same flag
# shape R12 used for `gig: true`. `anchor` is a HINT, not a contract: Lane B owns placement and is
# free to re-anchor. Gains are deliberately low because `gen_audio.encode()` runs loudnorm over
# every ambience asset, so "distant" cannot be shipped as a quiet render — it is shipped as
# spectrum in the synth, and as gain here.
AUDIO_LIFE = {
"mower-distant": ("ambience", 0.30, True, "town", "a mower two streets over"),
"radio-through-window": ("ambience", 0.25, True, "house", "someone's radio through a window"),
"sprinkler": ("ambience", 0.35, True, "yard", "impulse sprinkler on a lawn"),
"dog-bark-fence": ("sfx", 0.55, False, "yard", "the dog going off behind a paling fence"),
"roller-door": ("sfx", 0.50, False, "shop", "a roller door run up"),
"magpie-carol": ("sfx", 0.45, False, "tree", "the carol — the warble, not the clack"),
}
# ── depot-reuse: already-published GLBs we point at without a local normalize. The round-3 reuse
# check found vintage-cash-register.glb live on 3GOD (from thriftgod_hero) — reuse it for the
@ -94,6 +117,15 @@ GROUND_USE = {
"asphalt1": "road", "asphalt2": "road", "djsim-road": "road",
"footpath1": "footpath", "footpath2": "footpath", "djsim-footpath": "footpath",
"grass": "yard", "brickpave": "plaza", "gravel": "verge", "reddust": "outback",
# ── R38 / v8 wave 1.2 — the per-town ground palette, so 23 towns can look like 23 towns.
# ground.js merges each surface class into ONE town-wide mesh, so a palette swap is +0 draws
# / +0 tris. NOTE FOR LANE B (measured, R38): ground.js:177 hardcodes exactly FOUR slots —
# road / footpath / yard (the base plane) / plaza. `gravel` and `reddust` are selected by
# nothing partly because their `use` values ("verge", "outback") name no slot that exists.
# These four deliberately use live slot vocabulary so the new selector can consume them
# without a vocabulary change.
"grass-dry": "yard", "grass-temperate": "yard", "coastal-sand": "yard",
"bitumen-reddust": "road",
}
# ── curated: interior floor / surface split of the tex-* skins ───────────────────────────
FLOOR_TEX = ["carpet-swirl", "carpet-mustard", "carpet-greygreen", "lino-check", "lino-cork",
@ -151,6 +183,14 @@ def build_audio():
footsteps[surf] = vs
if footsteps:
sfx["footstep"] = footsteps
for k, (cat, g, lp, anchor, note) in AUDIO_LIFE.items(): # R38 inhabitation layer
r = ref(f"{cat}-{k}")
if not r:
continue
e = {**r, "gain": g, "scope": "street", "life": True, "anchor": anchor, "note": note}
if lp:
e["loop"] = True
(ambience if cat == "ambience" else sfx)[k] = e
for k in music: # flag gig-only beds for F (any 'gig-*' key)
if k.startswith("gig-"):
music[k]["gig"] = True
@ -232,7 +272,7 @@ def build():
fittings, furniture = {}, {}
# cube_shelf dropped: horizontal node-scale distortion (6.3x8.4m footprint) — see AUDIT.md
DROP = {"procity_fit_cube_shelf_01.glb"}
for results_file in (RESULTS, PROPS_RESULTS, GIG_RESULTS):
for results_file in (RESULTS, PROPS_RESULTS, GIG_RESULTS, R38_RESULTS):
if not os.path.exists(results_file):
continue
for r in json.load(open(results_file)).get("results", []):
@ -291,6 +331,13 @@ def build():
"suggested 0-1 playback level; interior roomtone/music carry types[] (seed-pick "
"per shop, like facades); footstep is {surface:[variants]}. Silent-happy: any "
"missing file => silence, never an error; ?mute=1 / ?noassets=1 => no audio fetch.",
"audio_life": "entries tagged life:true are the v8 wave-1.1 INHABITATION LAYER — point "
"sources for a life happening off-screen, played at a seeded anchor so it "
"is the same dog on the same street every time. `anchor` is a placement "
"HINT (town/house/yard/shop/tree), not a contract. playSfx is MONO with a "
"gain multiplier, so distance reads and direction does not: 'distance' is "
"baked into each asset's spectrum, not its level (loudnorm would undo a "
"quiet render). Zero draws, zero tris.",
},
"skins": skins,
"fittings": fittings,

View File

@ -514,6 +514,197 @@ def sfx_applause(name):
return peaknorm(fade(mix(out, cheer), 0.005, 0.15), -3)
# ─────────────────────────────────────────────────────────────────────────────
# ROUND 38 (v8 wave 1.1) — THE INHABITATION LAYER: someone else's life, off-screen.
#
# Six point sources for Lane B's seeded anchors. Same PACK discipline as everything above:
# deterministic through crc32(name), seamless where it loops, 🟢 original by construction (every
# sample is an oscillator or a noise render — there is no recording to imitate and nothing to
# clear). Zero draws, zero tris: the only item in the epoch that cannot touch the render budget.
#
# NOTE FOR LANE B, measured not assumed: `playSfx` is mono with a gain multiplier, so DISTANCE
# works and DIRECTION does not (R38 binding correction 1). These are all authored mono-compatible
# — the loops carry a little stereo width for the bed case, but nothing here depends on panning
# to read correctly. The "distance" in `mower-distant` is baked into the SPECTRUM (the top two
# octaves are gone, the way air actually filters a mower two streets over), not into the level,
# because `encode()` runs loudnorm over every `ambience` asset and would undo a quiet render.
# Ship the quietness as the manifest `gain`.
# ─────────────────────────────────────────────────────────────────────────────
def _bark(r, f0, dur):
"""One bark: a pitch-dropping glottal buzz through two vocal-tract formants, with a noise
burst on the attack. f0 ~150-210 Hz is a medium suburban dog."""
n = int(dur * SR)
f = np.linspace(f0 * 1.45, f0 * 0.82, n)
src = osc(f, dur, "saw")
nz = noise(dur, r) * np.exp(-np.linspace(0, 26, n)) * 0.5
x = src * 0.8 + nz
x = 0.55 * bandpass(x, 300, 1200) + 0.35 * bandpass(x, 900, 2600) + 0.2 * lp1(x, 300)
env = np.exp(-np.linspace(0, 7, n)) * (1 - np.exp(-np.linspace(0, 60, n)))
return x * env
def sfx_dog_bark_fence(name):
"""The dog going off behind a paling fence. A burst of 6-10 barks at an irregular rate — a dog
that has decided about you, not a single alert woof. The fence and the yard are the whole
character: a paling slap-back at ~28 ms and a lid on everything above 2.2 kHz."""
r = rng(name)
dur = 3.2
n = int(dur * SR)
out = np.zeros(n)
f0 = r.uniform(150, 210) # this dog's voice — fixed for the whole burst
t = 0.05
while t < dur - 0.35:
b = _bark(r, f0 * r.uniform(0.92, 1.10), r.uniform(0.13, 0.20))
p = int(t * SR); e = min(p + len(b), n)
out[p:e] += b[:e - p] * r.uniform(0.7, 1.0)
t += r.uniform(0.22, 0.46)
out = lp1(out, 2200) # timber + distance take the top
out = out + np.concatenate([np.zeros(int(0.028 * SR)), out])[:n] * 0.25 # paling slap-back
out = schroeder_reverb(out, 0.16, 0.45) # the yard it is standing in
return peaknorm(fade(out, 0.003, 0.08), -4)
def _warble(r, dur, f0, f1):
"""One fluted magpie note: a glide f0->f1 under a fast quaver (the syrinx tremolo), with the
2nd and 3rd harmonics that make it read as flute rather than whistle."""
n = int(dur * SR)
f = np.linspace(f0, f1, n) * (1 + 0.06 * np.sin(2 * np.pi * r.uniform(28, 52) * np.arange(n) / SR))
x = osc(f, dur, "sine") + 0.35 * osc(f * 2, dur, "sine") + 0.12 * osc(f * 3, dur, "sine")
return x * np.hanning(n)
def sfx_magpie_carol(name):
"""The CAROL — the warble, not the clack. A magpie has two sides to its syrinx and sings with
both at once, which is why the carol sounds like two birds in one throat: every note here is
doubled by a quieter voice roughly a fifth up. Open-air reverb, no roof."""
r = rng(name)
dur = 2.6
n = int(dur * SR)
out = np.zeros(n)
t = 0.06
while t < dur - 0.30:
d = r.uniform(0.10, 0.26)
f0 = r.uniform(900, 2400)
f1 = f0 * r.uniform(0.62, 1.55)
note = _warble(r, d, f0, f1) + 0.35 * _warble(r, d, f0 * 1.48, f1 * 1.42) # 2nd voice
p = int(t * SR); e = min(p + len(note), n)
out[p:e] += note[:e - p] * r.uniform(0.5, 1.0)
t += d + r.uniform(0.01, 0.09)
out = bandpass(out, 500, 7000)
out = schroeder_reverb(out, 0.22, 0.55)
return peaknorm(fade(out, 0.005, 0.10), -5)
def sfx_roller_door(name):
"""A corrugated roller door run up: a dense clatter of the curtain over the guides that speeds
up and slows down, the sheet-metal drum rumbling under it, the thin panel modes ringing only
while it rattles (amplitude-followed, so the ring can never sound detached), then the stop."""
r = rng(name)
dur = 2.8
n = int(dur * SR)
out = np.zeros(n)
t, tend = 0.04, dur - 0.35
while t < tend:
c = bandpass(noise(0.02, r), 1200, 7000) * np.exp(-np.linspace(0, 45, int(0.02 * SR)))
p = int(t * SR); e = min(p + len(c), n)
out[p:e] += c[:e - p] * r.uniform(0.3, 1.0)
rate = 0.020 + 0.030 * abs(math.cos(math.pi * min(t / tend, 1.0))) # slow-fast-slow
t += rate * r.uniform(0.7, 1.3)
rum = bandpass(noise(dur, r), 90, 420) * 0.6
rum *= np.clip(np.sin(np.pi * np.linspace(0, 1, n)) * 1.4, 0, 1)
follow = lp1(np.abs(out), 60)
follow = follow / (np.max(follow) or 1.0)
ring = (osc(196, dur, "sine") * 0.5 + osc(263, dur, "sine") * 0.3
+ osc(431, dur, "sine") * 0.2) * follow
clunk = np.zeros(n)
cp = int((dur - 0.30) * SR)
kl = _bell(43, 0.55, [(1, 1), (2.1, 0.5), (3.4, 0.25)], 9)
clunk[cp:cp + len(kl)] += kl[:n - cp] * 0.9
body = softclip(mix(out * 0.9, rum * 0.5, ring * 0.6, clunk) * 1.2, 1.2)
return peaknorm(fade(body, 0.003, 0.06), -3)
MOWER_LEN = 12.0
def amb_mower_distant(name):
"""A four-stroke mower two streets over. The firing pulse train wanders as it hits thick grass;
the whole thing is low-passed at 900 Hz because that is what a couple of hundred metres of air
and two rows of houses actually do to it. Every LFO is an exact harmonic of 1/MOWER_LEN and the
firing rate is rounded to a whole number of cycles per loop, so the loop genuinely folds and
the crossfade has almost nothing left to hide."""
r = rng(name)
dur = MOWER_LEN + XF
t = tarr(dur)
cyc = round(r.uniform(48, 58) * MOWER_LEN) # whole cycles per loop => phase folds
f = (cyc / MOWER_LEN) * (1 + 0.020 * np.sin(2 * np.pi * (3 / MOWER_LEN) * t)
+ 0.012 * np.sin(2 * np.pi * (1 / MOWER_LEN) * t + 1.1))
eng = softclip((osc(f, dur, "saw") * 0.6 + osc(f * 2, dur, "square") * 0.25
+ osc(f * 3, dur, "saw") * 0.12) * 1.4, 1.6)
blade = bandpass(noise(dur, r), 700, 3000) * 0.25 # the cut, hissing off the blade
whine = osc(f * 7.5, dur, "sine") * 0.05
body = lp1(mix(eng * 0.5, blade, whine), 900) # <- the distance, spent on spectrum
body = body * (0.55 + 0.45 * (0.5 + 0.5 * np.sin(2 * np.pi * (1 / MOWER_LEN) * t
+ r.random() * 6))) # pushed away, back
body = mix(pan(body, r.uniform(-0.4, 0.4)), pan(lp1(noise(dur, r, "pink"), 300) * 0.08, 0))
return seamless(peaknorm(body, -8), XF)
SPRINK_LEN = 8.0
def amb_sprinkler(name):
"""An impulse sprinkler on a lawn: the spray hiss sweeping as the head turns, and the arm
ticking its way across the arc then swinging back silently. The tick is the whole sound
a hard metal transient with a short descending body under it."""
r = rng(name)
dur = SPRINK_LEN + XF
n = int(dur * SR)
sweep = 0.35 + 0.65 * (0.5 + 0.5 * np.sin(2 * np.pi * (1 / SPRINK_LEN) * tarr(dur) - np.pi / 2))
# spray sits UNDER the arm: at 0.5 the hiss masked the ticks in the render (verified on the
# spectrogram) and it read as a hose, not a sprinkler. The tick is the sprinkler.
out = bandpass(noise(dur, r), 1800, 9000) * sweep * 0.28
tk = int(0.012 * SR)
t = 0.0
while t < dur:
if (t % SPRINK_LEN) / SPRINK_LEN < 0.62: # ticking arc
k = (bandpass(noise(0.012, r), 2500, 11000) * np.exp(-np.linspace(0, 55, tk))
+ osc(np.linspace(1400, 900, tk), 0.012, "sine") * np.exp(-np.linspace(0, 40, tk)) * 0.4)
p = int(t * SR); e = min(p + tk, n)
out[p:e] += k[:e - p] * r.uniform(0.55, 1.0)
t += r.uniform(0.085, 0.115)
else: # the swing back: no ticks
t += 0.02
return seamless(peaknorm(pan(lp1(out, 6000), r.uniform(-0.3, 0.3)), -8), XF)
def amb_radio_through_window(name):
"""A radio through a window: an ORIGINAL four-bar turnaround (parody-safe by construction, like
every music bed in this pack) played through a small tinny speaker, then through glass. The
band-limit is the point 380-2600 Hz is a windowsill radio, and the 1.5 kHz lid on top of that
is the window. You are not meant to be able to name the song; you are meant to know someone
is home."""
r = rng(name); bpm = 112; bars = 4
spb = 60 / bpm
cell = int(spb * SR)
prog = [50, 57, 55, 52] # D A G E — original voicing
bass = render_line(sum([[(root - 12, 1.0)] * 4 for root in prog], []), bpm,
lambda m, d: synth_note(m, d, "tri", 0.006, 0.10, 0.6, 0.06, cutoff=700))
chords = []
for root in prog:
for _ in range(4):
ch = mix(epiano(root + 12, spb * 0.85), epiano(root + 16, spb * 0.85),
epiano(root + 19, spb * 0.85)) * 0.5
chords.append(np.concatenate([ch, np.zeros(max(cell - len(ch), 0))])[:cell])
chords = np.concatenate(chords)
drums = render_drums({"k": [0, 8], "s": [4, 12], "h": [0, 2, 4, 6, 8, 10, 12, 14]}, bpm, bars, r)
body = mix(bass * 0.8, chords * 0.7, drums * 0.5)
body = softclip(bandpass(body, 380, 2600) * 1.15, 1.3) # the little speaker, driven
hiss = bandpass(noise(len(body) / SR, r), 1200, 5000) * 0.02 # AM tuning hiss
body = lp1(mix(body, hiss), 1500) # the glass
return loop_music(peaknorm(body, -10), 0.16)
def _footstep(name, bright, res_f):
r = rng(name)
thump = osc(np.linspace(res_f*1.6, res_f, int(0.05*SR)), 0.05, "sine")
@ -720,6 +911,14 @@ PACK = {
"music-gig-covers": ("music", music_covers, dict(loop=True, gain=0.5, note="gig live bed — covers (RSL); mellower, organ-led")),
"ambience-crowd-walla":("ambience", amb_crowd_walla, dict(loop=True, gain=0.4, note="gig crowd layer (interior)")),
"sfx-applause": ("sfx", sfx_applause, dict(loop=False, gain=0.7, note="between-sets cheer")),
# ── R38 / v8 wave 1.1 — THE INHABITATION LAYER (six point sources at seeded anchors; Lane B
# owns the mixer layers + falloff). Zero draws, zero tris. `life` flag in the manifest. ──
"ambience-mower-distant": ("ambience", amb_mower_distant, dict(loop=True, gain=0.30, note="a mower two streets over")),
"ambience-radio-through-window":("ambience", amb_radio_through_window,dict(loop=True, gain=0.25, note="someone's radio through a window")),
"ambience-sprinkler": ("ambience", amb_sprinkler, dict(loop=True, gain=0.35, note="impulse sprinkler on a lawn")),
"sfx-dog-bark-fence": ("sfx", sfx_dog_bark_fence, dict(loop=False, gain=0.55, note="the dog going off behind a paling fence")),
"sfx-roller-door": ("sfx", sfx_roller_door, dict(loop=False, gain=0.50, note="a roller door run up")),
"sfx-magpie-carol": ("sfx", sfx_magpie_carol, dict(loop=False, gain=0.45, note="the carol — the warble, not the clack")),
}

View File

@ -1,19 +1,31 @@
#!/usr/bin/env python3
"""PROCITY hero props — free, on-device (round-3 Fable path: MODELBEAST, not fal.ai).
Two local stages, both on the M3 Ultra GPU, zero cloud cost:
1. concept image flux_local (FLUX.2-klein-4B) text->object photo ~6s
2. mesh trellis_mac (TRELLIS.2-4B) image->GLB+PBR ~3-5 min (1024_cascade)
Three local stages, all on the M3 Ultra GPU, zero cloud cost:
1. concept image flux_local (FLUX.2-klein-4B) text->object photo ~6-9s
2. cutout bg_remove_local (RMBG-2.0) photo->RGBA subject ~5-10s
3. mesh trellis2_mlx (TRELLIS.2-4B MLX) image->GLB+PBR ~2-3 min (1024_cascade)
then pipeline/normalize.py (house GLB law) + thumbnail + manifest.
R38 PIPELINE FIX the operator swap (docs/REVIEW_CITY_PATTERNS.md §2, measured on the farm):
this script used to hardcode `trellis_mac` (p50 245.4s, 33 jobs/7d). The farm's workhorse is
**`trellis2_mlx`** (384 jobs/7d, p50 134.6s, better PBR bake) a ~2x throughput gain. The two
operators are NOT drop-in on output: trellis_mac writes one `*.glb`, trellis2_mlx writes BOTH
`candidate.glb` (raw, untextured) and `candidate_pbr.glb` (the baked one) into the same tree,
so a naive `sorted(rglob('*.glb'))[0]` silently picks the untextured mesh. We read `result.json`
(the operator's own declared output) and fall back to `candidate_pbr.glb` by name. `--legacy-trellis`
keeps the old operator available for an A/B.
Prop list + prompts come from pipeline/meshgod_batch.json (the same gap list that was going to
fal.ai). cash-register is skipped vintage-cash-register.glb already lives on the 3GOD depot.
python3 pipeline/gen_props.py --dry-run # list props + what's done, no GPU
python3 pipeline/gen_props.py --concepts # stage 1 for all -> .genprops/<name>.png
python3 pipeline/gen_props.py --concepts --only glass-case
python3 pipeline/gen_props.py --mesh --only glass-case # stage 2 for one (concept must exist)
python3 pipeline/gen_props.py --mesh # stage 2 for every prop with a concept, no glb
python3 pipeline/gen_props.py --mesh --only glass-case # stages 2+3 for one (concept must exist)
python3 pipeline/gen_props.py --mesh # stages 2+3 for every prop with a concept, no glb
python3 pipeline/gen_props.py --mesh --no-cutout # skip RMBG (concept already has alpha)
python3 pipeline/gen_props.py --mesh --legacy-trellis # the old trellis_mac path (A/B only)
python3 pipeline/gen_props.py --draft --only glass-case # sf3d seconds-fast draft (iterate concept)
Resumable: skips a stage whose output already exists in .genprops/. Eyeball .genprops/*.png (concepts)
@ -30,8 +42,15 @@ os.makedirs(RAW, exist_ok=True)
MB = os.environ.get("MB_HOME", os.path.expanduser("~/Documents/MODELBEAST"))
FLUX_PY = os.path.join(MB, "venvs/mflux/bin/python")
FLUX_RUN = os.path.join(MB, "server/operators/flux_local/run.py")
TRELLIS_PY = os.path.join(MB, "vendor/trellis-mac/.venv/bin/python")
# R38: trellis2_mlx is the farm's workhorse (p50 134.6s vs trellis_mac's 245.4s). Its operator
# run.py is stdlib-only and resolves the fork's own .venv internally, so the outer interpreter
# just has to exist; we use the fork venv for symmetry with the legacy line below.
TRELLIS2_PY = os.path.join(MB, "vendor/trellis2-mlx/.venv/bin/python")
TRELLIS2_RUN = os.path.join(MB, "server/operators/trellis2_mlx/run.py")
TRELLIS_PY = os.path.join(MB, "vendor/trellis-mac/.venv/bin/python") # legacy (--legacy-trellis)
TRELLIS_RUN = os.path.join(MB, "server/operators/trellis_mac/run.py")
RMBG_PY = os.path.join(MB, "venvs/rmbg/bin/python")
RMBG_RUN = os.path.join(MB, "server/operators/bg_remove_local/run.py")
SF3D_PY = os.path.join(MB, "venvs/sf3d/bin/python")
SF3D_RUN = os.path.join(MB, "server/operators/sf3d/run.py")
@ -61,6 +80,54 @@ def glb_path(name):
return os.path.join(RAW, f"{name}.glb")
def cutout_path(name):
return os.path.join(RAW, f"{name}.cut.png")
def gen_cutout(name):
"""concept photo -> RGBA subject cutout (bg_remove_local / RMBG-2.0). The mesh operators
segment internally, but feeding a clean alpha measurably improves thin-feature retention and
it is the path the R38 review measured. Fail-soft: caller falls back to the raw concept."""
src = concept_path(name)
outdir = os.path.join(RAW, "_x_" + name)
os.makedirs(outdir, exist_ok=True)
params = json.dumps({"resolution": 1024, "background": "transparent"})
r = subprocess.run([RMBG_PY, RMBG_RUN, "--input", src, "--outdir", outdir, "--params", params],
capture_output=True, text=True, timeout=600)
cut = os.path.join(outdir, os.path.splitext(os.path.basename(src))[0] + "_cutout.png")
if not os.path.exists(cut):
shutil.rmtree(outdir, ignore_errors=True)
raise RuntimeError((r.stderr or r.stdout or "no cutout")[-200:])
dst = cutout_path(name)
shutil.move(cut, dst)
shutil.rmtree(outdir, ignore_errors=True)
return dst
def _pick_glb(outdir):
"""The operator-output contract, made explicit. trellis2_mlx emits BOTH candidate.glb (raw)
and candidate_pbr.glb (baked) alphabetical order picks the WRONG one. Trust result.json
first (each operator declares its own output there), then the _pbr name, then anything."""
rj = os.path.join(outdir, "result.json")
if os.path.exists(rj):
try:
outs = json.load(open(rj)).get("outputs") or []
for o in outs:
p = o.get("path", "")
p = p if os.path.isabs(p) else os.path.join(outdir, p)
if p.endswith(".glb") and os.path.exists(p):
return p
except Exception:
pass
glbs = []
for dp, _, fs in os.walk(outdir):
glbs += [os.path.join(dp, f) for f in fs if f.endswith(".glb")]
if not glbs:
return None
pbr = [g for g in glbs if os.path.basename(g) == "candidate_pbr.glb"]
return (pbr or sorted(glbs))[0]
def gen_concept(a, seed=3):
name, prompt = a["name"], a["prompt"]
outdir = os.path.join(RAW, "_c_" + name)
@ -77,30 +144,45 @@ def gen_concept(a, seed=3):
return concept_path(name)
def gen_mesh(a, draft=False):
"""concept image -> GLB. TRELLIS 1024_cascade by default; sf3d for a fast draft."""
def gen_mesh(a, draft=False, legacy=False, cutout=True):
"""concept image -> GLB. trellis2_mlx 1024_cascade by default (R38 swap); sf3d for a fast
draft; --legacy-trellis for the old trellis_mac operator."""
name = a["name"]
cpath = concept_path(name)
if not os.path.exists(cpath):
raise RuntimeError(f"no concept image for {name} — run --concepts first")
if cutout and not draft:
if os.path.exists(cutout_path(name)):
cpath = cutout_path(name)
else:
try:
cpath = gen_cutout(name)
except Exception as e: # fail-soft: the mesh op segments anyway
print(f" [cutout] {name} skipped: {str(e)[:90]}")
outdir = os.path.join(RAW, "_m_" + name)
os.makedirs(outdir, exist_ok=True)
if draft:
params = json.dumps({"device": "mps", "texture_resolution": 1024, "foreground_ratio": 0.85})
cmd = [SF3D_PY, SF3D_RUN, "--input", cpath, "--outdir", outdir, "--params", params]
tmo = 600
else:
elif legacy:
params = json.dumps({"pipeline_type": "1024_cascade", "texture_size": 1024, "seed": 0})
cmd = [TRELLIS_PY, TRELLIS_RUN, "--input", cpath, "--outdir", outdir, "--params", params]
tmo = 1200
tmo = 1800
else:
# trellis2_mlx params_schema (manifest.json, verified R38): pipeline_type / seed / steps /
# baker / texture_size / max_bake_faces / alpha_mode. metal baker = UV PBR (meshgod-grade);
# 500k bake cap = fal parity and M3-Ultra-verified; opaque alpha kills the speckle veil.
params = json.dumps({"pipeline_type": "1024_cascade", "texture_size": 1024, "seed": 0,
"baker": "metal", "max_bake_faces": 500000, "alpha_mode": "opaque"})
cmd = [TRELLIS2_PY, TRELLIS2_RUN, "--input", cpath, "--outdir", outdir, "--params", params]
tmo = 1800
r = subprocess.run(cmd, capture_output=True, text=True, timeout=tmo)
glbs = []
for dp, _, fs in os.walk(outdir):
glbs += [os.path.join(dp, f) for f in fs if f.endswith(".glb")]
if not glbs:
src = _pick_glb(outdir)
if not src:
raise RuntimeError((r.stderr or r.stdout or "no glb")[-240:])
dst = glb_path(name)
shutil.move(sorted(glbs)[0], dst)
shutil.move(src, dst)
shutil.rmtree(outdir, ignore_errors=True)
return dst
@ -110,6 +192,8 @@ if __name__ == "__main__":
BATCH = os.path.abspath(sys.argv[sys.argv.index("--batch") + 1])
only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else ""
draft = "--draft" in sys.argv
legacy = "--legacy-trellis" in sys.argv
cutout = "--no-cutout" not in sys.argv
todo = [a for a in props() if not only or only in a["name"]]
if "--dry-run" in sys.argv:
@ -135,13 +219,14 @@ if __name__ == "__main__":
print(f"[concept] {a['name']} FAILED: {e}")
if do_mesh:
tool = "sf3d-draft" if draft else "trellis 1024_cascade"
tool = ("sf3d-draft" if draft else
("trellis_mac 1024_cascade" if legacy else "trellis2_mlx 1024_cascade"))
for a in todo:
if not draft and os.path.exists(glb_path(a["name"])):
print(f"[mesh] {a['name']} — glb exists, skip"); continue
t0 = time.time()
try:
dst = gen_mesh(a, draft=draft)
dst = gen_mesh(a, draft=draft, legacy=legacy, cutout=cutout)
kb = os.path.getsize(dst) // 1024
print(f"[mesh:{tool}] {a['name']}{round(time.time()-t0,1)}s ({kb}KB)")
except Exception as e:

View File

@ -66,6 +66,33 @@ POSTERS = {
"poster-retro": "colourful retro flyer, orange and teal, geometric 70s-revival shapes and a starburst",
}
# ── round-38 (v8 wave 1.2): GROUND palette. The 23 real towns differ only in street layout and
# shop names; ground.js merges each surface class into ONE town-wide mesh, so a palette swap is
# +0 draws / +0 tris. Grounds get their OWN style prefix: the house STYLE bible says "warm
# fluorescent lighting", which is an INTERIOR cue and wrong for an outdoor overhead surface.
# Square (1024²) by deliberate choice — grounds tile through RepeatWrapping, and the two djsim
# grounds (the ones that ship 512×512) are the correct precedent; the eight flux-era grounds are
# 512×279 and stretch under a square UV tile. See LANE_E_NOTES R38.
GROUND_STYLE = ("Photographic ground texture, flat overhead view, neutral overcast daylight, "
"slightly faded and worn, no watermarks, no text. ")
GROUND = ("Straight-down overhead photograph of {v}, camera pointing vertically at the ground, "
"completely flat-on with no perspective and no horizon, evenly lit with no cast shadows, "
"no objects, no people, no plants sticking up, a uniform repeating surface texture "
"filling the entire frame edge to edge")
GROUNDS = { # slug (→ web/assets/gen/ground-<slug>.jpg) : subject
"grass-dry": "dry Australian summer lawn, straw-blond and khaki, sun-bleached and "
"patchy, bare dusty soil showing through in places",
"grass-temperate": "a cool-temperate southern lawn, dense deep-green damp grass with a few "
"clover leaves and moss, evenly cropped",
# R38: the first roll of this one read as red PAINT SPATTER — discrete bright blobs, not dust.
# Rewritten to say "even film" and "no bright spots" explicitly; the second roll is what ships.
"bitumen-reddust": "worn grey bitumen road surface under an even film of fine red outback "
"dust, the dust lying evenly across the whole surface and drifted into "
"the cracks, dusty and matte, no bright spots, no paint, no road markings",
"coastal-sand": "pale coastal sand mixed with crushed shell grit, fine dry wind-rippled "
"beach sand",
}
PROMPTS = {
# ── shop-type facades that the registry is thin on ──────────────────────────────────
**{f"facade-{k}": FACADE.format(v=v) for k, v in {
@ -135,10 +162,11 @@ def local_available():
return os.path.exists(FLUX_PY) and os.path.exists(FLUX_RUN)
def gen_local(slug, prompt, w=1024, h=576, steps=4, seed=7):
def gen_local(slug, prompt, w=1024, h=576, steps=4, seed=7, style=None):
outdir = os.path.join(RAW, "_job_" + slug)
os.makedirs(outdir, exist_ok=True)
params = json.dumps({"prompt": STYLE + prompt, "model": "flux2-klein-4b",
params = json.dumps({"prompt": (STYLE if style is None else style) + prompt,
"model": "flux2-klein-4b",
"steps": steps, "width": w, "height": h, "seed": seed,
"quantize": "8", "guidance": 3.5})
r = subprocess.run([FLUX_PY, FLUX_RUN, "--outdir", outdir, "--params", params],
@ -194,6 +222,24 @@ def harvest():
if __name__ == "__main__":
if "--harvest" in sys.argv:
harvest(); sys.exit(0)
if "--grounds" in sys.argv: # round-38 ground palette (square tile, local only)
todo = {f"ground-{s}": GROUND.format(v=v) for s, v in GROUNDS.items()
if not have(f"ground-{s}")}
print(f"{len(todo)} ground skins (MODELBEAST flux2-klein-4b, local·free, 1024² tile)")
if "--dry-run" in sys.argv:
for s in sorted(todo):
print(f" {s}")
sys.exit(0)
if not local_available():
print(f"ERROR: MODELBEAST flux_local missing ({FLUX_RUN})."); sys.exit(2)
for i, (slug, prompt) in enumerate(sorted(todo.items()), 1):
try:
fn = gen_local(slug, prompt, w=1024, h=1024, seed=38, style=GROUND_STYLE)
print(f"[{i}/{len(todo)}] {slug} ({os.path.getsize(fn)//1024}KB)")
except Exception as e:
print(f"[{i}/{len(todo)}] {slug} FAILED: {str(e)[:90]}")
print("done. Review .genraw/ground-*.png, then --harvest → web/assets/gen/ground-*.jpg.")
sys.exit(0)
if "--posters" in sys.argv: # round-12 gig posters (portrait, local only)
todo = {s: POSTER.format(v=v) for s, v in POSTERS.items() if not have(s)}
print(f"{len(todo)} gig posters (MODELBEAST flux2-klein-4b, local·free)")

View File

@ -0,0 +1,42 @@
{
"_comment": "ROUND 38 (v8 WAVE 1) asset batch for pipeline/gen_props.py --batch. Two capability probes first (magpie_static, hills_hoist), then 1.4's backyard kit. Path: flux_local -> bg_remove_local -> trellis2_mlx (the R38 operator swap) -> normalize.py. $0, on-device, no cloud key. Every one of these is OPTIONAL at runtime: Lane B's boxMats primitives are the fallback and the review's own recommendation for the thin cases.",
"_probe": "hills-hoist is DELIBERATELY the hard case — thin wire/arm structures are the known TRELLIS failure class. A failure here is a decisive finding, not a defeat: the fallback is primitives, which boxMats wants anyway.",
"_r38_outcome": "5/5 generated first attempt, 0 retries, 0 OOM. trellis2_mlx median 128.0s (vs trellis_mac p50 245.4s = 1.92x), one 489s tail outlier. SHIPPED 3: magpie 894 tris (albedo flagged — TRELLIS crushed the white nape/wing to black; geometry good), paling_fence 1121 tris, water_tank 500 tris. FAILED 2 and DELETED from _normalized: hills-hoist and tv-aerial — thin structures return as thousands of disconnected fragments, so collapse floors at 250k/89k and a voxel remesh coarse enough for 600 tris erases the pole. Lane B uses primitives inside boxMats (+0 draws), which is strictly better. Full detail: AUDIT.md R38 §4.",
"assets": [
{
"name": "magpie",
"prompt": "a single Australian magpie bird standing on both feet, glossy black body and head, a broad white nape and white wing patches, a long straight pale blue-grey beak with a dark tip, wings folded against the body, side-on three-quarter pose, whole bird from beak to tail tip",
"height_m": 0.4,
"tris": 900,
"for": "1.3 the magpie (Lane B) — static mesh on a scripted parabola"
},
{
"name": "hills-hoist",
"prompt": "a rotary clothes hoist washing line, one galvanised steel centre pole on a small square concrete footing, four straight square arms spread horizontally at the top, thin parallel wire lines strung between the arms, nothing hanging on it",
"height_m": 2.0,
"tris": 600,
"for": "1.4 backyard Australia (Lane B, boxMats) — CAPABILITY PROBE, thin structure"
},
{
"name": "paling-fence-panel",
"prompt": "one straight section of Australian timber paling fence, about eight weathered grey vertical hardwood palings with small gaps, nailed to two horizontal rails behind them, flat panel seen straight on",
"height_m": 1.8,
"tris": 400,
"for": "1.4 backyard Australia — fence runs"
},
{
"name": "tv-aerial",
"prompt": "a rooftop television antenna, a horizontal aluminium boom carrying a row of thin straight cross elements of decreasing length and a small ladder reflector at the back, mounted on a short vertical mast",
"height_m": 1.2,
"tris": 400,
"for": "1.4 backyard Australia — roof aerials"
},
{
"name": "water-tank",
"prompt": "a round corrugated galvanised steel rainwater tank, a ribbed cylindrical wall with a shallow domed lid and a small outlet tap near the base, standing on the ground",
"height_m": 2.2,
"tris": 500,
"for": "1.4 backyard Australia — yard tanks"
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -9,7 +9,8 @@
"corner_lots": "corner lots expose a side wall: dedicated facade-<x>-side.jpg skins now exist for brick/render/weatherboard (face:'side') \u2014 Lane B maps these to the exposed side face; other types reuse the front skin",
"fallbacks": "every asset is optional \u2014 missing file => primitive/flat-colour, never a crash",
"load": "fittings/furniture load from depot as depot:<file>; skins are local gen/<file>",
"audio": "OGG/Opus primary + M4A/AAC fallback; loop:true beds are seamless; gain is a suggested 0-1 playback level; interior roomtone/music carry types[] (seed-pick per shop, like facades); footstep is {surface:[variants]}. Silent-happy: any missing file => silence, never an error; ?mute=1 / ?noassets=1 => no audio fetch."
"audio": "OGG/Opus primary + M4A/AAC fallback; loop:true beds are seamless; gain is a suggested 0-1 playback level; interior roomtone/music carry types[] (seed-pick per shop, like facades); footstep is {surface:[variants]}. Silent-happy: any missing file => silence, never an error; ?mute=1 / ?noassets=1 => no audio fetch.",
"audio_life": "entries tagged life:true are the v8 wave-1.1 INHABITATION LAYER \u2014 point sources for a life happening off-screen, played at a seeded anchor so it is the same dog on the same street every time. `anchor` is a placement HINT (town/house/yard/shop/tree), not a contract. playSfx is MONO with a gain multiplier, so distance reads and direction does not: 'distance' is baked into each asset's spectrum, not its level (loudnorm would undo a quiet render). Zero draws, zero tris."
},
"skins": {
"facade": {
@ -401,10 +402,18 @@
"file": "gen/ground-asphalt2.jpg",
"use": "road"
},
"bitumen-reddust": {
"file": "gen/ground-bitumen-reddust.jpg",
"use": "road"
},
"brickpave": {
"file": "gen/ground-brickpave.jpg",
"use": "plaza"
},
"coastal-sand": {
"file": "gen/ground-coastal-sand.jpg",
"use": "yard"
},
"djsim-footpath": {
"file": "gen/ground-djsim-footpath.jpg",
"use": "footpath"
@ -425,6 +434,14 @@
"file": "gen/ground-grass.jpg",
"use": "yard"
},
"grass-dry": {
"file": "gen/ground-grass-dry.jpg",
"use": "yard"
},
"grass-temperate": {
"file": "gen/ground-grass-temperate.jpg",
"use": "yard"
},
"gravel": {
"file": "gen/ground-gravel.jpg",
"use": "verge"
@ -996,6 +1013,36 @@
"scope": "interior",
"gig": true,
"note": "gig crowd layer (interior; F plays when gig on)"
},
"mower-distant": {
"file": "audio/ambience-mower-distant.ogg",
"fallback": "audio/ambience-mower-distant.m4a",
"gain": 0.3,
"scope": "street",
"life": true,
"anchor": "town",
"note": "a mower two streets over",
"loop": true
},
"radio-through-window": {
"file": "audio/ambience-radio-through-window.ogg",
"fallback": "audio/ambience-radio-through-window.m4a",
"gain": 0.25,
"scope": "street",
"life": true,
"anchor": "house",
"note": "someone's radio through a window",
"loop": true
},
"sprinkler": {
"file": "audio/ambience-sprinkler.ogg",
"fallback": "audio/ambience-sprinkler.m4a",
"gain": 0.35,
"scope": "street",
"life": true,
"anchor": "yard",
"note": "impulse sprinkler on a lawn",
"loop": true
}
},
"sfx": {
@ -1070,6 +1117,33 @@
"fallback": "audio/step-timber-3.m4a"
}
]
},
"dog-bark-fence": {
"file": "audio/sfx-dog-bark-fence.ogg",
"fallback": "audio/sfx-dog-bark-fence.m4a",
"gain": 0.55,
"scope": "street",
"life": true,
"anchor": "yard",
"note": "the dog going off behind a paling fence"
},
"roller-door": {
"file": "audio/sfx-roller-door.ogg",
"fallback": "audio/sfx-roller-door.m4a",
"gain": 0.5,
"scope": "street",
"life": true,
"anchor": "shop",
"note": "a roller door run up"
},
"magpie-carol": {
"file": "audio/sfx-magpie-carol.ogg",
"fallback": "audio/sfx-magpie-carol.m4a",
"gain": 0.45,
"scope": "street",
"life": true,
"anchor": "tree",
"note": "the carol \u2014 the warble, not the clack"
}
},
"music": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB