83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""mbgen.py — VINYLGOD asset gen via MODELBEAST (free, local farm).
|
|
|
|
Usage:
|
|
python3 tools/mbgen.py "prompt text" out.png # flux_local text->image
|
|
python3 tools/mbgen.py --op bg_remove_local in.png out.png
|
|
Token: MB_TOKEN env, else ~/Documents/backnforth/.env (never print it).
|
|
Contract per ~/Documents/MESHGOD/scripts/mb_recon.py (verified 2026-07-16):
|
|
POST /api/jobs -> poll /api/jobs/{id} -> match own asset by parent_job.
|
|
Job logs carry raw control chars; strip before json.loads.
|
|
"""
|
|
import json, mimetypes, os, sys, time, urllib.request, uuid
|
|
|
|
HOST = os.environ.get('MB_HOST', 'http://100.89.131.57:8777')
|
|
|
|
|
|
def token():
|
|
t = os.environ.get('MB_TOKEN')
|
|
if t:
|
|
return t
|
|
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 {})
|
|
body = urllib.request.urlopen(urllib.request.Request(HOST + path, data=data, headers=h), timeout=120).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 upload(path):
|
|
boundary = uuid.uuid4().hex
|
|
ctype = mimetypes.guess_type(path)[0] or 'application/octet-stream'
|
|
body = (f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{os.path.basename(path)}"\r\n'
|
|
f'Content-Type: {ctype}\r\n\r\n').encode() + open(path, 'rb').read() + f'\r\n--{boundary}--\r\n'.encode()
|
|
a = req('/api/assets', data=body, headers={'Content-Type': f'multipart/form-data; boundary={boundary}'})
|
|
return a.get('id') or (a.get('items') or [a])[0].get('id')
|
|
|
|
|
|
def run(op, params, out, asset_id=None):
|
|
payload = {'operator': op, 'params': params}
|
|
if asset_id:
|
|
payload['asset_id'] = asset_id
|
|
j = req('/api/jobs', data=json.dumps(payload).encode(), headers={'Content-Type': 'application/json'})
|
|
jid = j['id']
|
|
print(f'[mb] job {jid} ({op}) submitted', flush=True)
|
|
t0 = time.time()
|
|
while True:
|
|
time.sleep(5)
|
|
st = req(f'/api/jobs/{jid}').get('status')
|
|
print(f'[mb] {st} ({int(time.time() - t0)}s)', flush=True)
|
|
if st in ('done', 'error', 'cancelled'):
|
|
break
|
|
if st != 'done':
|
|
sys.exit(f'[mb] job ended {st}')
|
|
a = req('/api/assets?limit=100')
|
|
items = a if isinstance(a, list) else a.get('items', [])
|
|
mine = [x for x in items if x.get('parent_job') == jid]
|
|
if not mine:
|
|
sys.exit('[mb] no output asset with matching parent_job')
|
|
# some operators emit a params .json alongside the real output — skip it
|
|
media = [x for x in mine if not str(x.get('name', '')).endswith('.json')] or mine
|
|
open(out, 'wb').write(req(f"/api/assets/{media[0]['id']}/file", raw=True))
|
|
print(f'[mb] wrote {out} ({os.path.getsize(out) // 1024}KB)')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
args = sys.argv[1:]
|
|
op = 'flux_local'
|
|
if args and args[0] == '--op':
|
|
op = args[1]
|
|
args = args[2:]
|
|
if op == 'flux_local':
|
|
run(op, {'prompt': args[0]}, args[1])
|
|
else: # input-file operators
|
|
run(op, {}, args[1], asset_id=upload(args[0]))
|