diff --git a/E-progress.md b/E-progress.md index 9da926a..55914dc 100644 --- a/E-progress.md +++ b/E-progress.md @@ -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:** ~8–12 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 diff --git a/docs/LANES/LANE_E_NOTES.md b/docs/LANES/LANE_E_NOTES.md index c838f46..b7a9589 100644 --- a/docs/LANES/LANE_E_NOTES.md +++ b/docs/LANES/LANE_E_NOTES.md @@ -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). --- diff --git a/docs/shots/laneE/props_fal.png b/docs/shots/laneE/props_fal.png new file mode 100644 index 0000000..0313f52 Binary files /dev/null and b/docs/shots/laneE/props_fal.png differ diff --git a/pipeline/AUDIT.md b/pipeline/AUDIT.md index 029d73d..6827803 100644 --- a/pipeline/AUDIT.md +++ b/pipeline/AUDIT.md @@ -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. diff --git a/pipeline/_props_results.json b/pipeline/_props_results.json index dd86378..c74eea0 100644 --- a/pipeline/_props_results.json +++ b/pipeline/_props_results.json @@ -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": [] diff --git a/pipeline/bake_lowpoly.py b/pipeline/bake_lowpoly.py index a1e4c1e..799ae6a 100644 --- a/pipeline/bake_lowpoly.py +++ b/pipeline/bake_lowpoly.py @@ -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") diff --git a/pipeline/batch_fal.json b/pipeline/batch_fal.json new file mode 100644 index 0000000..763d13b --- /dev/null +++ b/pipeline/batch_fal.json @@ -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} +] diff --git a/pipeline/build_manifest.py b/pipeline/build_manifest.py index 4e6c789..e31b64f 100644 --- a/pipeline/build_manifest.py +++ b/pipeline/build_manifest.py @@ -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} diff --git a/pipeline/gen_props_fal.py b/pipeline/gen_props_fal.py new file mode 100644 index 0000000..76adc0a --- /dev/null +++ b/pipeline/gen_props_fal.py @@ -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 ~$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() diff --git a/pipeline/meshgod_batch.json b/pipeline/meshgod_batch.json index ecbb87a..847fc59 100644 --- a/pipeline/meshgod_batch.json +++ b/pipeline/meshgod_batch.json @@ -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"}, diff --git a/web/assets/manifest.json b/web/assets/manifest.json index 6567017..a263734 100644 --- a/web/assets/manifest.json +++ b/web/assets/manifest.json @@ -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" } } \ No newline at end of file diff --git a/web/assets/thumbs/procity_fit_glass_case_01.png b/web/assets/thumbs/procity_fit_glass_case_01.png new file mode 100644 index 0000000..9cbb1f3 Binary files /dev/null and b/web/assets/thumbs/procity_fit_glass_case_01.png differ diff --git a/web/assets/thumbs/procity_fit_magazine_rack_01.png b/web/assets/thumbs/procity_fit_magazine_rack_01.png new file mode 100644 index 0000000..5969ef3 Binary files /dev/null and b/web/assets/thumbs/procity_fit_magazine_rack_01.png differ diff --git a/web/assets/thumbs/procity_street_bus_shelter_01.png b/web/assets/thumbs/procity_street_bus_shelter_01.png new file mode 100644 index 0000000..261a084 Binary files /dev/null and b/web/assets/thumbs/procity_street_bus_shelter_01.png differ