diff --git a/docs/LANES/LANE_E_NOTES.md b/docs/LANES/LANE_E_NOTES.md index 6d368e5..1a44195 100644 --- a/docs/LANES/LANE_E_NOTES.md +++ b/docs/LANES/LANE_E_NOTES.md @@ -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 — diff --git a/docs/shots/laneE/inhabitation_spectrograms.png b/docs/shots/laneE/inhabitation_spectrograms.png new file mode 100644 index 0000000..8300264 Binary files /dev/null and b/docs/shots/laneE/inhabitation_spectrograms.png differ diff --git a/docs/shots/laneE/r38_magpie_albedo_probe.png b/docs/shots/laneE/r38_magpie_albedo_probe.png new file mode 100644 index 0000000..5f3ada3 Binary files /dev/null and b/docs/shots/laneE/r38_magpie_albedo_probe.png differ diff --git a/docs/shots/laneE/r38_props_shipped.png b/docs/shots/laneE/r38_props_shipped.png new file mode 100644 index 0000000..8b24995 Binary files /dev/null and b/docs/shots/laneE/r38_props_shipped.png differ diff --git a/docs/shots/laneE/r38_thin_structure_failure.png b/docs/shots/laneE/r38_thin_structure_failure.png new file mode 100644 index 0000000..cfe6166 Binary files /dev/null and b/docs/shots/laneE/r38_thin_structure_failure.png differ diff --git a/pipeline/AUDIT.md b/pipeline/AUDIT.md index 47a85c3..0feedd6 100644 --- a/pipeline/AUDIT.md +++ b/pipeline/AUDIT.md @@ -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.** diff --git a/pipeline/_r38_results.PENDING_PUBLISH.json b/pipeline/_r38_results.PENDING_PUBLISH.json new file mode 100644 index 0000000..8a515b6 --- /dev/null +++ b/pipeline/_r38_results.PENDING_PUBLISH.json @@ -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": [] +} \ No newline at end of file diff --git a/pipeline/batch_r38.json b/pipeline/batch_r38.json new file mode 100644 index 0000000..614a0c2 --- /dev/null +++ b/pipeline/batch_r38.json @@ -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 + } +] diff --git a/pipeline/build_manifest.py b/pipeline/build_manifest.py index 0d484a7..2857d5c 100644 --- a/pipeline/build_manifest.py +++ b/pipeline/build_manifest.py @@ -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, diff --git a/pipeline/gen_audio.py b/pipeline/gen_audio.py index fd23485..6b8a173 100644 --- a/pipeline/gen_audio.py +++ b/pipeline/gen_audio.py @@ -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")), } diff --git a/pipeline/gen_props.py b/pipeline/gen_props.py index a51a5cb..be1900b 100644 --- a/pipeline/gen_props.py +++ b/pipeline/gen_props.py @@ -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/.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: diff --git a/pipeline/gen_skins.py b/pipeline/gen_skins.py index ac83c62..a185810 100644 --- a/pipeline/gen_skins.py +++ b/pipeline/gen_skins.py @@ -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-.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)") diff --git a/pipeline/meshgod_batch_r38.json b/pipeline/meshgod_batch_r38.json new file mode 100644 index 0000000..e56ec3f --- /dev/null +++ b/pipeline/meshgod_batch_r38.json @@ -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" + } + ] +} diff --git a/web/assets/audio/ambience-mower-distant.m4a b/web/assets/audio/ambience-mower-distant.m4a new file mode 100644 index 0000000..8fafe1b Binary files /dev/null and b/web/assets/audio/ambience-mower-distant.m4a differ diff --git a/web/assets/audio/ambience-mower-distant.ogg b/web/assets/audio/ambience-mower-distant.ogg new file mode 100644 index 0000000..08d3cc7 Binary files /dev/null and b/web/assets/audio/ambience-mower-distant.ogg differ diff --git a/web/assets/audio/ambience-radio-through-window.m4a b/web/assets/audio/ambience-radio-through-window.m4a new file mode 100644 index 0000000..5911172 Binary files /dev/null and b/web/assets/audio/ambience-radio-through-window.m4a differ diff --git a/web/assets/audio/ambience-radio-through-window.ogg b/web/assets/audio/ambience-radio-through-window.ogg new file mode 100644 index 0000000..18972b5 Binary files /dev/null and b/web/assets/audio/ambience-radio-through-window.ogg differ diff --git a/web/assets/audio/ambience-sprinkler.m4a b/web/assets/audio/ambience-sprinkler.m4a new file mode 100644 index 0000000..217aa9d Binary files /dev/null and b/web/assets/audio/ambience-sprinkler.m4a differ diff --git a/web/assets/audio/ambience-sprinkler.ogg b/web/assets/audio/ambience-sprinkler.ogg new file mode 100644 index 0000000..baf5807 Binary files /dev/null and b/web/assets/audio/ambience-sprinkler.ogg differ diff --git a/web/assets/audio/sfx-dog-bark-fence.m4a b/web/assets/audio/sfx-dog-bark-fence.m4a new file mode 100644 index 0000000..7feab89 Binary files /dev/null and b/web/assets/audio/sfx-dog-bark-fence.m4a differ diff --git a/web/assets/audio/sfx-dog-bark-fence.ogg b/web/assets/audio/sfx-dog-bark-fence.ogg new file mode 100644 index 0000000..c4013c5 Binary files /dev/null and b/web/assets/audio/sfx-dog-bark-fence.ogg differ diff --git a/web/assets/audio/sfx-magpie-carol.m4a b/web/assets/audio/sfx-magpie-carol.m4a new file mode 100644 index 0000000..b1cba89 Binary files /dev/null and b/web/assets/audio/sfx-magpie-carol.m4a differ diff --git a/web/assets/audio/sfx-magpie-carol.ogg b/web/assets/audio/sfx-magpie-carol.ogg new file mode 100644 index 0000000..0acd342 Binary files /dev/null and b/web/assets/audio/sfx-magpie-carol.ogg differ diff --git a/web/assets/audio/sfx-roller-door.m4a b/web/assets/audio/sfx-roller-door.m4a new file mode 100644 index 0000000..0c8fb87 Binary files /dev/null and b/web/assets/audio/sfx-roller-door.m4a differ diff --git a/web/assets/audio/sfx-roller-door.ogg b/web/assets/audio/sfx-roller-door.ogg new file mode 100644 index 0000000..589f266 Binary files /dev/null and b/web/assets/audio/sfx-roller-door.ogg differ diff --git a/web/assets/gen/ground-bitumen-reddust.jpg b/web/assets/gen/ground-bitumen-reddust.jpg new file mode 100644 index 0000000..089bb25 Binary files /dev/null and b/web/assets/gen/ground-bitumen-reddust.jpg differ diff --git a/web/assets/gen/ground-coastal-sand.jpg b/web/assets/gen/ground-coastal-sand.jpg new file mode 100644 index 0000000..a2b99cf Binary files /dev/null and b/web/assets/gen/ground-coastal-sand.jpg differ diff --git a/web/assets/gen/ground-grass-dry.jpg b/web/assets/gen/ground-grass-dry.jpg new file mode 100644 index 0000000..b0be0c7 Binary files /dev/null and b/web/assets/gen/ground-grass-dry.jpg differ diff --git a/web/assets/gen/ground-grass-temperate.jpg b/web/assets/gen/ground-grass-temperate.jpg new file mode 100644 index 0000000..4899939 Binary files /dev/null and b/web/assets/gen/ground-grass-temperate.jpg differ diff --git a/web/assets/manifest.json b/web/assets/manifest.json index a765305..a32e51d 100644 --- a/web/assets/manifest.json +++ b/web/assets/manifest.json @@ -9,7 +9,8 @@ "corner_lots": "corner lots expose a side wall: dedicated facade--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:; skins are local gen/", - "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": { diff --git a/web/assets/thumbs/procity_street_magpie_01.png b/web/assets/thumbs/procity_street_magpie_01.png new file mode 100644 index 0000000..8eeabf3 Binary files /dev/null and b/web/assets/thumbs/procity_street_magpie_01.png differ diff --git a/web/assets/thumbs/procity_street_paling_fence_01.png b/web/assets/thumbs/procity_street_paling_fence_01.png new file mode 100644 index 0000000..e13f64d Binary files /dev/null and b/web/assets/thumbs/procity_street_paling_fence_01.png differ diff --git a/web/assets/thumbs/procity_street_water_tank_01.png b/web/assets/thumbs/procity_street_water_tank_01.png new file mode 100644 index 0000000..aaee993 Binary files /dev/null and b/web/assets/thumbs/procity_street_water_tank_01.png differ