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>
100 lines
3.8 KiB
Python
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])
|