John's editor taxonomy: kink/A-frame/donkey/down/square rails, pole jam, hubba, low/table boxes, inclined manny, pyramid, euro gap, vert quarter, wallride wall, volcano, roller, jersey barrier, parking block, hydrant, pool coping, chain fence, floodlight, bleachers, drain grate. propkit: alpha + emissive materials; lathe winding fixed (was inside-out). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
6.5 KiB
Python
124 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""gen_textures.py — batch park-surface textures from MODELBEAST flux_local.
|
|
|
|
Guest token allows 4 active jobs: submit in a rolling window, poll, download
|
|
each job's own output (parent_job match — newest-asset raced once, 2026-07-16).
|
|
Token from ~/Documents/backnforth/.env (never printed). Re-runs skip textures
|
|
that already exist on disk, so the script is idempotent.
|
|
|
|
Usage: python3 tools/gen_textures.py [--force name ...]
|
|
"""
|
|
import json, os, sys, time, urllib.request
|
|
|
|
HOST = os.environ.get('MB_HOST', 'http://100.89.131.57:8777')
|
|
OUT = os.path.join(os.path.dirname(__file__), '..', 'textures')
|
|
|
|
# Every prompt asks for flat, even, shadow-free top-down macro photography —
|
|
# these get tiled with MIRRORED_REPEAT in the GLB sampler, which hides seams,
|
|
# but baked-in lighting gradients would still show. 1024px, klein-4b, 4 steps.
|
|
STYLE = ('seamless tileable texture, photographed straight down, perfectly flat, '
|
|
'even diffuse lighting, no shadows, no vignette, full-frame surface detail, '
|
|
'photorealistic macro material photograph')
|
|
|
|
TEXTURES = {
|
|
'concrete_smooth': 'smooth grey skatepark concrete, steel-trowel finish, faint swirl marks, fine aggregate flecks, light wear',
|
|
'concrete_green': 'green tinted polished skatepark concrete, smooth burnished surface, subtle colour variation, small speckle aggregate',
|
|
'concrete_rough': 'broom finished pale concrete footpath surface, fine parallel brush lines, weathered',
|
|
'plywood_ramp': 'worn skate ramp plywood, birch surface scuffed with black urethane wheel marks and scratches, faded grain',
|
|
'steel_galv': 'galvanized steel sheet metal, crystalline spangle pattern, dull grey zinc coating',
|
|
'steel_scratch': 'steel surface covered in fine bright grind scratches and wax residue, well used skateboard rail metal',
|
|
'wood_slats': 'weathered grey-brown hardwood park bench timber, deep grain, small cracks, sun faded',
|
|
'bark_fig': 'Moreton Bay fig tree bark, grey-brown, rough woven fibrous ridges',
|
|
'leaves_fig': 'dense dark green glossy fig tree canopy foliage seen from below, layered leaves',
|
|
'bark_gum': 'eucalyptus gum tree bark, smooth mottled cream grey and salmon patches, peeling strips',
|
|
'leaves_gum': 'dense eucalyptus foliage filling the entire frame, layered slender sage-green curved gum leaves, no sky visible',
|
|
'grass_dry': 'dry australian park grass, green-brown patchy couch lawn, some bare dirt showing',
|
|
'tarmac': 'grey asphalt road surface, fine bitumen aggregate, small stones, light wear',
|
|
'dirt_mulch': 'garden bed surface of brown dirt and eucalyptus bark mulch chips',
|
|
'brick_qld': 'reddish brown queensland house bricks with pale mortar joints, running bond',
|
|
'graffiti_a': 'colorful graffiti burner piece with bold wildstyle letters on a concrete skatepark wall, aerosol art, blues oranges purples',
|
|
'graffiti_b': 'concrete wall covered in layered spray paint tags and simple throw-up graffiti letters, black silver white paint',
|
|
'tiles_pool': 'vintage swimming pool tile band, small square ceramic tiles, white with pale smalt-blue accent rows, aged grout lines',
|
|
'steel_grate': 'steel drainage grate with parallel slot openings, dark weathered galvanized metal',
|
|
'sky_day': 'clear bright queensland blue sky with a few small white cumulus clouds, wide even gradient',
|
|
}
|
|
|
|
def token():
|
|
for line in open(os.path.expanduser('~/Documents/backnforth/.env')):
|
|
if line.startswith('MB_TOKEN='):
|
|
return line.split('=', 1)[1].strip()
|
|
sys.exit('no MB_TOKEN')
|
|
|
|
def req(path, data=None, headers=None, raw=False):
|
|
h = {'Authorization': f'Bearer {token()}'}
|
|
h.update(headers or {})
|
|
r = urllib.request.Request(HOST + path, data=data, headers=h)
|
|
body = urllib.request.urlopen(r, timeout=180).read()
|
|
if raw:
|
|
return body
|
|
s = body.decode('utf-8', 'replace')
|
|
return json.loads(''.join(c if c >= ' ' or c in '\t' else ' ' for c in s))
|
|
|
|
def submit(name, desc, seed):
|
|
p = {'operator': 'flux_local',
|
|
'params': {'prompt': f'{desc}, {STYLE}', 'model': 'flux2-klein-4b',
|
|
'steps': 4, 'width': 1024, 'height': 1024, 'seed': seed}}
|
|
j = req('/api/jobs', data=json.dumps(p).encode(),
|
|
headers={'Content-Type': 'application/json'})
|
|
print(f'[tx] {name}: job {j["id"]}', flush=True)
|
|
return j['id']
|
|
|
|
def fetch(jid, name):
|
|
items = req('/api/assets?limit=100')
|
|
items = items if isinstance(items, list) else items.get('items', [])
|
|
mine = [x for x in items if x.get('parent_job') == jid]
|
|
imgs = [x for x in mine if str(x.get('filename', x.get('name', ''))).lower()
|
|
.endswith(('.png', '.jpg', '.jpeg', '.webp'))]
|
|
if not imgs:
|
|
print(f'[tx] {name}: NO OUTPUT ASSET for job {jid}', flush=True)
|
|
return False
|
|
ext = os.path.splitext(str(imgs[0].get('filename', imgs[0].get('name', 'x.png'))))[1] or '.png'
|
|
data = req(f'/api/assets/{imgs[0]["id"]}/file', raw=True)
|
|
out = os.path.join(OUT, name + ext)
|
|
open(out, 'wb').write(data)
|
|
print(f'[tx] {name}: saved {len(data)//1024}KB -> {out}', flush=True)
|
|
return True
|
|
|
|
def main():
|
|
force = set(sys.argv[sys.argv.index('--force') + 1:]) if '--force' in sys.argv else set()
|
|
todo = []
|
|
for i, (name, desc) in enumerate(TEXTURES.items()):
|
|
have = [f for f in os.listdir(OUT) if f.startswith(name + '.')]
|
|
if have and name not in force:
|
|
print(f'[tx] {name}: exists, skip', flush=True)
|
|
continue
|
|
todo.append((name, desc, 1000 + i))
|
|
active = {} # jid -> name
|
|
fails = []
|
|
while todo or active:
|
|
while todo and len(active) < 4:
|
|
name, desc, seed = todo.pop(0)
|
|
try:
|
|
active[submit(name, desc, seed)] = name
|
|
except Exception as e:
|
|
print(f'[tx] {name}: submit failed {e}', flush=True)
|
|
fails.append(name)
|
|
time.sleep(7)
|
|
for jid in list(active):
|
|
try:
|
|
st = req(f'/api/jobs/{jid}').get('status')
|
|
except Exception:
|
|
continue
|
|
if st in ('done', 'error', 'cancelled'):
|
|
name = active.pop(jid)
|
|
if st == 'done':
|
|
if not fetch(jid, name):
|
|
fails.append(name)
|
|
else:
|
|
print(f'[tx] {name}: job {st}', flush=True)
|
|
fails.append(name)
|
|
print(f'[tx] COMPLETE. fails: {fails or "none"}', flush=True)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|