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>
@ -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 5–6 s RMBG cutout stage) vs `trellis_mac`'s 245.4 s p50 = **1.92×**. 5/5 first attempt, **0
|
||||
retries used** against the 4–5% 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 —
|
||||
|
||||
BIN
docs/shots/laneE/inhabitation_spectrograms.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/shots/laneE/r38_magpie_albedo_probe.png
Normal file
|
After Width: | Height: | Size: 332 KiB |
BIN
docs/shots/laneE/r38_props_shipped.png
Normal file
|
After Width: | Height: | Size: 623 KiB |
BIN
docs/shots/laneE/r38_thin_structure_failure.png
Normal file
|
After Width: | Height: | Size: 880 KiB |
@ -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 **5–6 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 28–52 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) → 380–2600 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.33–0.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/1–4k/>4k, carol **86% in 1–4 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 5–6 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 4–5% 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.**
|
||||
|
||||
53
pipeline/_r38_results.PENDING_PUBLISH.json
Normal 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
@ -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
|
||||
}
|
||||
]
|
||||
@ -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,
|
||||
|
||||
@ -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")),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)")
|
||||
|
||||
42
pipeline/meshgod_batch_r38.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
web/assets/audio/ambience-mower-distant.m4a
Normal file
BIN
web/assets/audio/ambience-mower-distant.ogg
Normal file
BIN
web/assets/audio/ambience-radio-through-window.m4a
Normal file
BIN
web/assets/audio/ambience-radio-through-window.ogg
Normal file
BIN
web/assets/audio/ambience-sprinkler.m4a
Normal file
BIN
web/assets/audio/ambience-sprinkler.ogg
Normal file
BIN
web/assets/audio/sfx-dog-bark-fence.m4a
Normal file
BIN
web/assets/audio/sfx-dog-bark-fence.ogg
Normal file
BIN
web/assets/audio/sfx-magpie-carol.m4a
Normal file
BIN
web/assets/audio/sfx-magpie-carol.ogg
Normal file
BIN
web/assets/audio/sfx-roller-door.m4a
Normal file
BIN
web/assets/audio/sfx-roller-door.ogg
Normal file
BIN
web/assets/gen/ground-bitumen-reddust.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
web/assets/gen/ground-coastal-sand.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
web/assets/gen/ground-grass-dry.jpg
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
web/assets/gen/ground-grass-temperate.jpg
Normal file
|
After Width: | Height: | Size: 180 KiB |
@ -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": {
|
||||
|
||||
BIN
web/assets/thumbs/procity_street_magpie_01.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
web/assets/thumbs/procity_street_paling_fence_01.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
web/assets/thumbs/procity_street_water_tank_01.png
Normal file
|
After Width: | Height: | Size: 46 KiB |