Lane E round 5 (E3): fal Hunyuan glass_case + bus_shelter + magazine_rack (~$0.64)

The 2 R3 TRELLIS glass rejects + optional magazine-rack, generated on fal.ai
Hunyuan3D v2 (John-authorized, $22 balance). pipeline/gen_props_fal.py drives the
fal operator using the FAL_KEY from MODELBEAST settings (never hardcoded). Frosted/
opaque flux_local concepts gave the reconstructor a surface (clear glass defeated
TRELLIS). glass-case 1 att, bus-shelter 2 att (opaque back wall), magazine-rack 1 att
= ~$0.64 total, guardrail held. All baked to <=8k via bake_lowpoly.py (switched to
EMIT albedo bake — Hunyuan materials bake near-black under DIFFUSE). Verified loading
in vendored GLTFLoader, published, validate --depot 0 err. Manifest 17 fittings + 9
furniture; aliases wire glass_case/glass_cabinets/magazine_rack/fridge/listening_corner.
Costs logged in AUDIT R5. Last 2 primitive-only fittings retired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-14 21:20:44 +10:00
parent 5671a06aef
commit 8644d3f7ac
14 changed files with 223 additions and 8 deletions

View File

@ -14,10 +14,16 @@ other lanes' files was touched.*
eyeballed (`docs/shots/laneE/props_baked.png` — fridge needed a coarser voxel re-bake, rest clean)
+ verified loading in the vendored GLTFLoader. Re-published; `validate --depot` 0 err. Originals
gitignored in `pipeline/.props_orig/`. → Lane C re-measures the record interior (was ~273k).
- **E3 — fal.ai fallbacks (authorized, decision #4):** glass-case + bus-shelter (+ optional
magazine-rack) via fal Hunyuan v2, ≤2 attempts each, costs logged in AUDIT. *(in progress)*
- **E3 — fal.ai fallbacks: DONE.** glass_case + bus_shelter + magazine_rack via fal Hunyuan3D v2
(`pipeline/gen_props_fal.py`, key from MODELBEAST settings). Frosted/opaque concepts (`flux_local`)
gave the reconstructor a surface — all 3 succeeded where TRELLIS failed. Baked to ≤8k (EMIT albedo
bake — Hunyuan materials need base-colour→emission or DIFFUSE bakes near-black), verified loading,
published, `validate --depot` 0 err. **Total fal spend ~$0.64** (glass 1 att, bus 2 att, mag 1
att × ~$0.16), well under the <$1 estimate; guardrail held. Costs logged in AUDIT R5. Last two
primitive-only fittings (glass_case, magazine_rack) retired; new street `bus_shelter`. Manifest
now 17 fittings + 9 furniture (26 GLBs). Contact sheet `docs/shots/laneE/props_fal.png`.
- **E2 — depot growth for v2:** ~812 library props serving C's dig / D's roster / B's windows.
*(in progress)*
*(next)*
## Round 4 (Fable) — ped decimation (gate-3 critical path) + asset fixes

View File

@ -21,9 +21,27 @@ baked (no separate metallic/normal maps → also dodges the record_crate danglin
**Please re-measure the record interior** (was ~273k tris in your R4 note) and record the new
number in LANE_C_GLB_VALIDATION.md — it should drop by ~most of the prop weight.
### → Fable: E1 (bake) status
DONE + committed. 6 props ≤8k, published, `validate --depot` 0 err, qa gate green. Feeds C's
re-measure + your v1.1 tag. E3 (fal glass-case/bus-shelter) + E2 (depot growth) next.
### → Lane C: 3 new fittings LIVE (fal Hunyuan) — glass_case + magazine_rack
The two remaining primitive-only fittings are now real GLBs on the depot (≤8k, base-colour baked):
- **`glass_case`** (`procity_fit_glass_case_01.glb`, 1.08×1.42 m, 1.0 m tall) — timber base + glass
panels + items inside. Registry ids `glass_case` (toy/dept) + `glass_cabinets` (pawn) both alias
to it. → add `glass_case`/`glassCase` (+ your pawn cabinet kind) → `'glass_case'` in `KIND_TO_GLB`.
- **`magazine_rack`** (`procity_fit_magazine_rack_01.glb`, 0.54×0.44 m, 1.2 m tall) — tiered timber
rack w/ magazines (milkbar/pawn). Registry `magazine_rack` aliases to it. → map your kind → `'magazine_rack'`.
- Both base-origin, verified loading in the vendored GLTFLoader. `manifest.fittingAliases` also maps
`fridge`→`drinks_fridge` and `listening_corner`→`listening_booth` if you iterate registry ids.
### → Lane B: new street prop — `bus_shelter`
`procity_street_bus_shelter_01.glb` (manifest `furniture.bus_shelter`, 2.33×1.41 m, 2.4 m tall, ≤8k,
base-origin). Solid painted-steel shelter (back wall + roof + bench) for street dressing / D's roster
context — place at bus-stop-ish spots along the footpath if useful. Primitive-free; fail-soft as usual.
### → Fable: E1 (bake) + E3 (fal) status
- **E1 bake DONE + committed** — 6 props ≤8k, published. Feeds C's re-measure + your v1.1 tag.
- **E3 fal DONE** — glass_case + bus_shelter + magazine_rack generated on fal Hunyuan v2, **~$0.64
total** (log in AUDIT R5), ≤2 attempts each, all baked ≤8k + published. Guardrail held (nothing
else on fal). `validate --depot` 0 err. Last two primitive-only fittings retired.
- **E2 depot growth** next (v2-serving library props).
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -219,3 +219,28 @@ and **verified loading in the vendored `GLTFLoader`** (tris ≤8k, baked texture
Re-published to the depot (`validate --depot` 0 errors). **Provenance unchanged: 100% synthetic/
on-device** (FLUX.2-klein-4B + TRELLIS.2 + Blender bake, no third-party source). → Lane C
re-measures the record interior (was ~273k tris).
## Round 5 — E3: fal.ai fallback props (glass-case + bus-shelter + magazine-rack)
The two R3 TRELLIS glass rejects + the optional magazine-rack, generated on **fal.ai Hunyuan3D v2**
(John-authorized, decision #4). Pipeline: `flux_local` concept (frosted/opaque surfaces so the
reconstructor has something to grab — clear glass is what defeated TRELLIS) → `pipeline/gen_props_fal.py`
(fal `fal-ai/hunyuan3d/v2`, key from MODELBEAST settings, never hardcoded) → `normalize.py` (scale/
origin) → `bake_lowpoly.py` (voxel-remesh → ≤8k → **EMIT albedo bake**; Hunyuan materials need the
base-colour→emission route, DIFFUSE(COLOR) baked them near-black).
**Paid gen log (fal Hunyuan3D v2, ~$0.16/gen):**
| prop | attempts | cost | outcome |
|---|---|---|---|
| glass-case | 1 (frosted-glass concept) | ~$0.16 | ✅ solid case, timber base, items visible — TRELLIS couldn't; 40k→8k baked |
| bus-shelter | 2 (att.1 dropped the thin perspex panels → att.2 with an opaque back wall) | ~$0.32 | ✅ solid shelter, back wall + roof + bench; 40k→8k |
| magazine-rack | 1 (optional; solid-timber, not wire) | ~$0.16 | ✅ tiered timber rack w/ magazines — beat the R3 wire failure; 40k→8k |
**Total fal spend this round: ~$0.64** of the $22 balance (well under the <$1 estimate). Guardrail
held — nothing else on fal. All 3 ≤8k tris, base-colour baked, **verified loading in the vendored
GLTFLoader**, eyeballed (`docs/shots/laneE/props_fal.png` — no melted glass, no legible branding),
published, `validate --depot` 0 err. **Provenance: FLUX.2-klein-4B concept + fal Hunyuan3D v2 mesh +
Blender bake — synthetic, no third-party source** (Hunyuan output is model-generated geometry).
Raw fal GLBs + concepts kept in gitignored `pipeline/.genprops/`. These retire the last two
primitive-only fittings (glass_case, magazine_rack) + add a street bus_shelter.

View File

@ -101,6 +101,54 @@
1.61,
0.61
]
},
{
"file": "procity_fit_glass_case_01.glb",
"tri_before": 40000,
"tri_after": 7999,
"size_m": [
1.085,
0.997,
1.418
],
"footprint": [
1.08,
1.42
],
"thumb": "/tmp/fal_bake/thumbs/procity_fit_glass_case_01.png",
"out": "/tmp/fal_bake/procity_fit_glass_case_01.glb"
},
{
"file": "procity_street_bus_shelter_01.glb",
"tri_before": 40000,
"tri_after": 7998,
"size_m": [
2.333,
2.4,
1.414
],
"footprint": [
2.33,
1.41
],
"thumb": "/tmp/fal_bake/thumbs/procity_street_bus_shelter_01.png",
"out": "/tmp/fal_bake/procity_street_bus_shelter_01.glb"
},
{
"file": "procity_fit_magazine_rack_01.glb",
"tri_before": 40000,
"tri_after": 8000,
"size_m": [
0.545,
1.2,
0.443
],
"footprint": [
0.54,
0.44
],
"thumb": "/tmp/fal_bake/thumbs/procity_fit_magazine_rack_01.png",
"out": "/tmp/fal_bake/procity_fit_magazine_rack_01.glb"
}
],
"errors": []

View File

@ -88,7 +88,31 @@ def make_lowpoly(hp, voxel, tris):
return lp
def _base_color_to_emission(hp):
"""Route every HP material's base-colour (texture or flat value) into its Emission and mute the
BSDF, so a bake_type='EMIT' pass reads PURE ALBEDO regardless of the source material setup
(TRELLIS and fal-Hunyuan wire their materials differently; DIFFUSE(COLOR) mis-reads fal's)."""
for m in hp.data.materials:
if not m or not m.use_nodes:
continue
nt = m.node_tree
b = nt.nodes.get("Principled BSDF")
if not b:
continue
bc, ec = b.inputs.get("Base Color"), (b.inputs.get("Emission Color") or b.inputs.get("Emission"))
es = b.inputs.get("Emission Strength")
if ec is None:
continue
if bc and bc.is_linked:
nt.links.new(bc.links[0].from_socket, ec)
elif bc:
ec.default_value = bc.default_value
if es is not None:
es.default_value = 1.0
def bake_color(hp, lp, img_size):
_base_color_to_emission(hp)
img = bpy.data.images.new("baked", img_size, img_size)
mat = bpy.data.materials.new("baked_mat"); mat.use_nodes = True
nt = mat.node_tree
@ -113,7 +137,7 @@ def bake_color(hp, lp, img_size):
bpy.ops.object.select_all(action='DESELECT')
hp.select_set(True); lp.select_set(True)
bpy.context.view_layer.objects.active = lp # active = bake TARGET
bpy.ops.object.bake(type='DIFFUSE', pass_filter={'COLOR'}, use_clear=True)
bpy.ops.object.bake(type='EMIT', use_clear=True) # HP base-colour routed to emission = albedo
# self-illuminate a touch so it reads in any viewer (matches normalize.py bright)
ec = bsdf.inputs.get("Emission Color") or bsdf.inputs.get("Emission")

5
pipeline/batch_fal.json Normal file
View File

@ -0,0 +1,5 @@
[
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/glass_case_fal1.glb", "id": "glass_case", "category": "fit", "height_m": 1.0, "tris": 60000},
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/bus_shelter_fal2.glb", "id": "bus_shelter", "category": "street", "height_m": 2.4, "tris": 60000},
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/magazine_rack_fal1.glb", "id": "magazine_rack", "category": "fit", "height_m": 1.2, "tris": 60000}
]

View File

@ -173,6 +173,9 @@ def build():
"clothes_racks": "clothes_rack", "bricabrac_shelving": "wire_shelf", "book_wall": "bookshelf",
"cube_shelves": "cube_shelf_wide", "display_tables": "coffee_table",
"bookshelves": "bookshelf", "vhs_shelving": "wire_shelf", "trestle_tables": "work_table",
# round-5 fal/MODELBEAST hero props → their registry semantic ids
"glass_case": "glass_case", "glass_cabinets": "glass_case", "magazine_rack": "magazine_rack",
"fridge": "drinks_fridge", "listening_corner": "listening_booth",
}
fitting_aliases = {k: v for k, v in fitting_aliases.items() if v in fittings}

48
pipeline/gen_props_fal.py Normal file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""fal.ai Hunyuan3D v2 image->GLB for the two TRELLIS glass rejects (glass-case, bus-shelter).
Paid John-authorized (ROUND5 decision #4; account topped up). ~14-17c/gen, <=2 attempts each,
every gen logged to AUDIT.md by the caller. Uses the owner's FAL_KEY configured in MODELBEAST
settings (never hardcoded here) via MODELBEAST's own settings loader — the same env-injection the
server does. MUST run with MODELBEAST's server venv python (it has fal_client):
MBPY=~/Documents/MODELBEAST/.venv/bin/python
$MBPY pipeline/gen_props_fal.py CONCEPT.png OUT.glb
Prints `FAL_OK <glb> ~$0.16` on success (cost is the caller's to record), `FAL_ERR ...` on failure.
"""
import sys, os, subprocess, json, glob, shutil
MB = os.path.expanduser("~/Documents/MODELBEAST")
sys.path.insert(0, MB)
from server import db, settings as S # noqa: E402
ENDPOINT = "fal-ai/hunyuan3d/v2" # ~$0.16 geo (+ texture); see MESHGOD_BATCH.md pricing
COST = 0.16
def main():
concept, out = sys.argv[1], sys.argv[2]
con = db.connect()
env = os.environ.copy()
env.update(S.env_for_operator(S.get_all(con), ["FAL_KEY"]))
if not env.get("FAL_KEY"):
print("FAL_ERR FAL_KEY not configured in MODELBEAST settings"); sys.exit(2)
opdir = os.path.join(MB, "server/operators/fal_hunyuan3d")
outdir = os.path.join(os.path.dirname(out) or ".", "_fal_" + os.path.basename(out).replace(".glb", ""))
os.makedirs(outdir, exist_ok=True)
params = json.dumps({"seed": 0, "textured_mesh": True})
r = subprocess.run([sys.executable, os.path.join(opdir, "run.py"),
"--input", concept, "--outdir", outdir, "--params", params],
env=env, capture_output=True, text=True, timeout=600)
glbs = glob.glob(os.path.join(outdir, "**", "*.glb"), recursive=True)
if not glbs:
print("FAL_ERR " + (r.stderr or r.stdout or "no glb")[-300:]); sys.exit(1)
shutil.move(sorted(glbs)[0], out)
shutil.rmtree(outdir, ignore_errors=True)
print(f"FAL_OK {out} ~${COST:.2f} ({ENDPOINT})")
if __name__ == "__main__":
main()

View File

@ -8,6 +8,12 @@
"fal_fallback": {"_comment": "TRELLIS failed on these transparent-glass subjects (no GLB); sf3d gives a featureless blob. Local 2-attempt budget spent -> fal.ai, gated on John (<=$2.70). Both have in-engine primitive fallbacks so nothing blocks.", "glass-case": "transparent glass top defeats TRELLIS segmentation", "bus-shelter": "thin steel frame + perspex panels"},
"note_tris": "TRELLIS meshes are dense multi-shell scans; normalize.py decimate floors 25-77k tris (>library <=5k law). Accepted for focal/optional/LOD-able props (texture-intact); a texture-bake-to-lowpoly / gltfpack pass is a follow-up."
},
"_round5_outcome": {
"_comment": "Round 5 (Fable): fal.ai authorized ($22 balance). The R3 fal_fallback trio is now DONE + live on the depot. Pipeline: flux_local concept (frosted/opaque surfaces) -> pipeline/gen_props_fal.py (fal-ai/hunyuan3d/v2) -> normalize -> bake_lowpoly (<=8k, EMIT albedo bake). ALSO: all 6 R3/R4 TRELLIS hero props baked to <=8k (pipeline/bake_lowpoly.py). Costs logged in AUDIT.md R5.",
"fal_done": {"glass-case": "1 attempt ~$0.16", "bus-shelter": "2 attempts ~$0.32 (att.2 opaque back wall)", "magazine-rack": "1 attempt ~$0.16 (optional, solid timber not wire)"},
"fal_total": "~$0.64 of $22; guardrail held (nothing else on fal)",
"baked_to_8k": ["drinks_fridge", "milkshake_mixer", "arcade_cabinet", "listening_booth", "novelty_record", "counter_till", "glass_case", "bus_shelter", "magazine_rack"]
},
"assets": [
{"name": "listening-booth", "prompt": "a record shop listening station booth, angled timber panel with a shelf and hanging headphones, single stool", "height_m": 1.6, "tris": 6000, "for": "record"},
{"name": "cash-register", "prompt": "an ornate antique brass cash register with round number keys and a bell", "height_m": 0.35, "tris": 6000, "for": "all", "reuse": "thriftgod_hero:vintage-cash-register — check depot first"},

View File

@ -693,6 +693,24 @@
"height": 1.146,
"thumb": "thumbs/procity_fit_counter_till_01.png"
},
"glass_case": {
"file": "procity_fit_glass_case_01.glb",
"footprint": [
1.08,
1.42
],
"height": 0.997,
"thumb": "thumbs/procity_fit_glass_case_01.png"
},
"magazine_rack": {
"file": "procity_fit_magazine_rack_01.glb",
"footprint": [
0.54,
0.44
],
"height": 1.2,
"thumb": "thumbs/procity_fit_magazine_rack_01.png"
},
"cash_register": {
"file": "vintage-cash-register.glb",
"footprint": [
@ -774,6 +792,15 @@
],
"height": 1.992,
"thumb": "thumbs/procity_street_novelty_record_01.png"
},
"bus_shelter": {
"file": "procity_street_bus_shelter_01.glb",
"footprint": [
2.33,
1.41
],
"height": 2.4,
"thumb": "thumbs/procity_street_bus_shelter_01.png"
}
},
"fittingAliases": {
@ -787,6 +814,11 @@
"display_tables": "coffee_table",
"bookshelves": "bookshelf",
"vhs_shelving": "wire_shelf",
"trestle_tables": "work_table"
"trestle_tables": "work_table",
"glass_case": "glass_case",
"glass_cabinets": "glass_case",
"magazine_rack": "magazine_rack",
"fridge": "drinks_fridge",
"listening_corner": "listening_booth"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB