park_kit/tools/gen_textures.py
type-two 153e26c329 park_kit v0.1 — 27 textured park props + 18 MODELBEAST surfaces + manifest
propkit.py: textured-GLB writer (TEXCOORD_0, shared external textures,
MIRRORED_REPEAT). prop_author.py: parametric generators — ramps, rails,
furniture, trees, dressing — each with local-space grind metadata and
skatemakerpro element/collider hints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:47:34 +10:00

122 lines
6.2 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',
'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()