- gen_props.py: flux_local -> trellis_mac -> normalize pipeline; 5/8 props kept (arcade-cabinet, listening-booth, drinks-fridge, milkshake-mixer, novelty-record) - magazine-rack -> primitive (thin-wire gotcha); glass-case + bus-shelter -> fal.ai fallback - cash-register reused from depot; all 22 GLBs live, validate --depot 0/0 - loaders.js ?localdepot=1 + stage_local_depot.py for network-free GLB validation - qa.sh --strict GREEN 4/4 at commit time Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
148 lines
6.8 KiB
Python
148 lines
6.8 KiB
Python
#!/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)
|
|
then pipeline/normalize.py (house GLB law) + thumbnail + manifest.
|
|
|
|
Prop list + prompts come from pipeline/meshgod_batch.json (the same gap list that was going to
|
|
fal.ai). cash-register is skipped — vintage-cash-register.glb already lives on the 3GOD depot.
|
|
|
|
python3 pipeline/gen_props.py --dry-run # list props + what's done, no GPU
|
|
python3 pipeline/gen_props.py --concepts # stage 1 for all -> .genprops/<name>.png
|
|
python3 pipeline/gen_props.py --concepts --only glass-case
|
|
python3 pipeline/gen_props.py --mesh --only glass-case # stage 2 for one (concept must exist)
|
|
python3 pipeline/gen_props.py --mesh # stage 2 for every prop with a concept, no glb
|
|
python3 pipeline/gen_props.py --draft --only glass-case # sf3d seconds-fast draft (iterate concept)
|
|
|
|
Resumable: skips a stage whose output already exists in .genprops/. Eyeball .genprops/*.png (concepts)
|
|
and the rendered GLBs before normalizing. Everything here is OPTIONAL at runtime — every prop has an
|
|
in-engine primitive fallback (see MESHGOD_BATCH.md _primitives_instead), so nothing blocks on it.
|
|
"""
|
|
import json, os, sys, subprocess, shutil, time
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
RAW = os.path.join(ROOT, "pipeline", ".genprops")
|
|
BATCH = os.path.join(ROOT, "pipeline", "meshgod_batch.json")
|
|
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")
|
|
TRELLIS_RUN = os.path.join(MB, "server/operators/trellis_mac/run.py")
|
|
SF3D_PY = os.path.join(MB, "venvs/sf3d/bin/python")
|
|
SF3D_RUN = os.path.join(MB, "server/operators/sf3d/run.py")
|
|
|
|
# Clean product-shot framing so TRELLIS/sf3d (which segment the subject) get a whole, isolated
|
|
# object on a plain backdrop — NOT the faded op-shop facade look. Period-appropriate but neutral.
|
|
CONCEPT = ("Clean studio product photograph of {v}. Single object, centred, entire object in "
|
|
"frame, three-quarter view, plain seamless pale-grey backdrop, soft even lighting, "
|
|
"sharp focus, no text, no people, no other objects, 1990s Australian period-correct.")
|
|
|
|
|
|
def props():
|
|
data = json.load(open(BATCH))
|
|
out = []
|
|
for a in data["assets"]:
|
|
# cash-register: reuse the depot's vintage-cash-register.glb (round-3 reuse check)
|
|
if a["name"] == "cash-register":
|
|
continue
|
|
out.append(a)
|
|
return out
|
|
|
|
|
|
def concept_path(name):
|
|
return os.path.join(RAW, f"{name}.png")
|
|
|
|
|
|
def glb_path(name):
|
|
return os.path.join(RAW, f"{name}.glb")
|
|
|
|
|
|
def gen_concept(a, seed=3):
|
|
name, prompt = a["name"], a["prompt"]
|
|
outdir = os.path.join(RAW, "_c_" + name)
|
|
os.makedirs(outdir, exist_ok=True)
|
|
params = json.dumps({"prompt": CONCEPT.format(v=prompt), "model": "flux2-klein-4b",
|
|
"steps": 4, "width": 1024, "height": 1024, "seed": seed})
|
|
r = subprocess.run([FLUX_PY, FLUX_RUN, "--outdir", outdir, "--params", params],
|
|
capture_output=True, text=True, timeout=300)
|
|
pngs = [p for p in os.listdir(outdir) if p.endswith(".png")]
|
|
if not pngs:
|
|
raise RuntimeError((r.stderr or r.stdout or "no png")[-200:])
|
|
shutil.move(os.path.join(outdir, pngs[0]), concept_path(name))
|
|
shutil.rmtree(outdir, ignore_errors=True)
|
|
return concept_path(name)
|
|
|
|
|
|
def gen_mesh(a, draft=False):
|
|
"""concept image -> GLB. TRELLIS 1024_cascade by default; sf3d for a fast draft."""
|
|
name = a["name"]
|
|
cpath = concept_path(name)
|
|
if not os.path.exists(cpath):
|
|
raise RuntimeError(f"no concept image for {name} — run --concepts first")
|
|
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:
|
|
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
|
|
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:
|
|
raise RuntimeError((r.stderr or r.stdout or "no glb")[-240:])
|
|
dst = glb_path(name)
|
|
shutil.move(sorted(glbs)[0], dst)
|
|
shutil.rmtree(outdir, ignore_errors=True)
|
|
return dst
|
|
|
|
|
|
if __name__ == "__main__":
|
|
only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else ""
|
|
draft = "--draft" in sys.argv
|
|
todo = [a for a in props() if not only or only in a["name"]]
|
|
|
|
if "--dry-run" in sys.argv:
|
|
print(f"{len(todo)} props (cash-register reused from depot):")
|
|
for a in todo:
|
|
c = "concept✓" if os.path.exists(concept_path(a["name"])) else "concept·"
|
|
g = "glb✓" if os.path.exists(glb_path(a["name"])) else "glb·"
|
|
print(f" {a['name']:16s} {c} {g} h={a['height_m']}m for={a['for']}")
|
|
sys.exit(0)
|
|
|
|
do_concepts = "--concepts" in sys.argv or (not "--mesh" in sys.argv and not draft)
|
|
do_mesh = "--mesh" in sys.argv or draft
|
|
|
|
if do_concepts:
|
|
for a in todo:
|
|
if os.path.exists(concept_path(a["name"])):
|
|
print(f"[concept] {a['name']} — exists, skip"); continue
|
|
t0 = time.time()
|
|
try:
|
|
gen_concept(a)
|
|
print(f"[concept] {a['name']} — {round(time.time()-t0,1)}s")
|
|
except Exception as e:
|
|
print(f"[concept] {a['name']} FAILED: {e}")
|
|
|
|
if do_mesh:
|
|
tool = "sf3d-draft" if draft else "trellis 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)
|
|
kb = os.path.getsize(dst) // 1024
|
|
print(f"[mesh:{tool}] {a['name']} — {round(time.time()-t0,1)}s ({kb}KB)")
|
|
except Exception as e:
|
|
print(f"[mesh:{tool}] {a['name']} FAILED: {e}")
|
|
print("done. Eyeball .genprops/*.png (concepts) + rendered GLBs before normalize.")
|