BLOBBO/tools/mesh_pipeline.py
type-two d4d41208f8 M0 playable slice: lanes integrated, machine chain staged, hidden-tab hardening
Integration: blob+paint+machines composed in game.ts; cannons staged along the
racing line (muzzle 26 — default 16 undershoots course distances); plate
threshold above clean-blob mass so only painted blobs trip it (paint=weight=
machine access); belt delivers riders under the bucket (shallow catch timed to
teeter drift); arch moved to a detour lane.

Cross-lane fixes found at integration, all one class — gameplay logic living
on render-frame time, which stalls in hidden tabs:
- telegraph windup -> fixed step (gates forces/dumps)
- bucket tip/pour  -> fixed step (gates the splat)
- blob group sync  -> fixed step in controller (paint proximity gate reads it)
world.ts grew tick()/renderOnce() + a hidden-tab watchdog interval for this.

Assets: mesh wave 1 (4 GLBs via farm: blobbo-base, toaster, spring boot,
paint bucket) + mesh_pipeline.py with outdir scp fallback for the farm's
unregistered-remote-outputs bug.

Verified headless end-to-end via tick(): move/jump/squash, cannon hits on a
moving blob, coverage->buffs (BURN 1.6x, super at 70%), mass 1.0->1.67 with
paint, plate clean-reject + painted-trip, boot launch (peak y 3.9), belt->
bucket dump (23.6% purple), arch cleanse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:50:16 +10:00

100 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""mesh_pipeline.py — flux image -> bg_remove -> image->GLB, per spec entry.
Usage: mesh_pipeline.py tools/mesh-wave-1.json assets/meshes/
Retries each farm job up to 3x (the m4 node's broken mflux venv 126s any flux
job routed to it — resubmission re-rolls the load-balancer). Skips any entry
whose .glb already exists, so reruns are cheap.
"""
import json, os, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mb
RETRIES = 3
def run_job(operator, params=None, asset_id=None, label=''):
for attempt in range(1, RETRIES + 1):
jid = None
while jid is None:
jid = mb.submit(operator, params, asset_id=asset_id)
if jid is None:
print(f'[pipe] {label}: throttled, 15s', flush=True)
time.sleep(15)
st = mb.wait(jid, f'{label} ({operator} try {attempt})', interval=10)
if st == 'done':
return jid
time.sleep(5)
raise RuntimeError(f'{label}: {operator} failed {RETRIES}x')
PRIMARY = 'm3ultra@100.89.131.57'
def scp_from_outdir(jid, out, suffix='.glb'):
"""Fallback for a farm bug (2026-07-17): hunyuan jobs run on remote workers
ship outputs back to the primary's job outdir but never register them as
assets. Pull the textured mesh (not *_shape.glb) straight from outdir."""
import subprocess
j = mb.req(f'/api/jobs/{jid}')
outdir = j.get('outdir')
if not outdir:
return False
ls = subprocess.run(['ssh', '-o', 'ConnectTimeout=8', '-o', 'BatchMode=yes',
PRIMARY, f'ls {outdir}'], capture_output=True, text=True)
names = [n for n in ls.stdout.split()
if n.endswith(suffix) and not n.endswith('_shape.glb')]
if not names:
return False
r = subprocess.run(['scp', '-o', 'ConnectTimeout=8', '-o', 'BatchMode=yes',
f'{PRIMARY}:{outdir}/{names[0]}', out], capture_output=True)
return r.returncode == 0 and os.path.exists(out)
def fetch_output(jid, out, suffix=None):
outs = mb.outputs_for(jid, suffix)
if outs:
n = mb.fetch(outs[0]['id'], out)
print(f'[pipe] wrote {out} ({n//1024}KB)', flush=True)
return
if suffix and scp_from_outdir(jid, out, suffix):
print(f'[pipe] wrote {out} ({os.path.getsize(out)//1024}KB, outdir fallback)', flush=True)
return
raise RuntimeError(f'no output asset for {jid} (api + outdir fallback both empty)')
def main(spec_path, outdir):
specs = json.load(open(spec_path))
srcdir = os.path.join(outdir, 'src')
os.makedirs(srcdir, exist_ok=True)
failed = []
for s in specs:
name = s['name']
glb = os.path.join(outdir, f'{name}.glb')
if os.path.exists(glb):
print(f'[pipe] {name}: glb exists, skip', flush=True)
continue
try:
img = os.path.join(srcdir, f'{name}.png')
if not os.path.exists(img):
jid = run_job('flux_local', {'prompt': s['prompt']}, label=name)
fetch_output(jid, img)
cut = os.path.join(srcdir, f'{name}-cut.png')
if not os.path.exists(cut):
aid = mb.upload(img)
jid = run_job('bg_remove_local', {}, asset_id=aid, label=name)
fetch_output(jid, cut)
aid = mb.upload(cut)
jid = run_job('hunyuan3d_mlx', {}, asset_id=aid, label=name)
fetch_output(jid, glb, suffix='.glb')
except Exception as e:
print(f'[pipe] {name}: FAILED — {e}', flush=True)
failed.append(name)
ok = len(specs) - len(failed)
print(f'[pipe] mesh wave complete: {ok}/{len(specs)} ok'
+ (f' — failed: {failed}' if failed else ''), flush=True)
if __name__ == '__main__':
main(sys.argv[1], sys.argv[2])