The flat cut-outs were the cheapest content on the board and the only tier with a shipping consumer, but wardrobegod couldn't show one: scan() filtered PNGs out of garments/ (fixed in Phase 0) and there was no compositor, no slots, no preview. Now there is a working dress-up. · library/doll/ seeded with 90sDJsim's 441 shipped sprites (165 top, 110 bottom, 60 shoes, 57 hat, base body) — an instant starter wardrobe rather than a cold start. · tools/doll.py wraps djsim's compositor. It deliberately does NOT import doll_composite: that module pulls numpy at import time for a keyer and an HSV recolour we never use (the farm's RMBG-2.0 does our keying), and numpy isn't installable here under PEP 668. Instead the measured anchors are parsed out of its source with ast, so there is still exactly one source of truth and they cannot drift; place() is a pure-PIL port. · slugify() reproduces djsim's item_art() _art_slug character for character, verified against it. That equality IS the drop-in contract — a near-miss silently falls back to generic slot art instead of erroring, so it's the kind of bug you'd ship without noticing. · /api/doll/gen runs the proven pipeline: flux_local on SOLID GREEN -> farm RMBG-2.0 -> staged to 704x1408. Green is not a preference: grey and checkerboard backgrounds eat garments in the key, and flux can't spell, so no text ever goes in the prompt. · /api/doll/compose composes server-side with PIL, so the browser preview and the exported PNG are the same bytes rather than two renderers that drift. · /api/doll/export writes into a LIVE game directory, so it defaults to a dry run and refuses any stem without _i_ — overwriting a generic slot fallback would restyle every unarted item in a shipping game. · UI: a doll 2D tab with per-slot pickers, randomise, strip, and export. Verified: composed a dressed doll from real sprites (correct base->bottom->shoes->top->hat order), catalogue reports 392 layers, export dry-run and the generic-art guard both behave. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
143 lines
6.2 KiB
Python
143 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""archetype_batch.py — the 12-archetype factory run (all local, all free).
|
|
|
|
Per archetype: flux_local sheet (solid green, A-pose, FITTED clothing — open vests are
|
|
the hard rig case) -> bg_remove_local -> trellis_mac mesh -> MIRPAMO preclean+proxy rig
|
|
(on the box) -> retarget wan_walk_t1 -> library/banks/<name>_at_wanwalk.glb.
|
|
|
|
Idempotent: skips any archetype whose bank already exists. Progress to stdout (run it
|
|
under nohup/tee). MB flow per the house client pattern (control-char-safe, parent_job).
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
import uuid
|
|
|
|
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
BANKS = os.path.join(HERE, 'library', 'banks')
|
|
SHEETS = os.path.join(HERE, 'library', 'sheets')
|
|
HOST = 'http://100.89.131.57:8777'
|
|
BOX = 'm3ultra@100.89.131.57'
|
|
os.makedirs(SHEETS, exist_ok=True)
|
|
|
|
ARCHETYPES = {
|
|
'nanna': 'an elderly Australian woman in a floral dress and a buttoned lavender cardigan, grey permed hair, flat shoes',
|
|
'postie': 'an Australia Post postal worker in 1990s uniform, red polo shirt and navy shorts, wide-brim hat, sturdy shoes',
|
|
'goth': 'a young goth woman in 1990s style, long black fitted dress, black boots, dark bob haircut, pale makeup',
|
|
'skater': 'a teenage skater boy in 1990s style, fitted graphic t-shirt, knee-length shorts, skate shoes, backwards cap',
|
|
'surfie': 'a tanned Australian surfer man in 1990s boardshorts and a fitted rash vest, barefoot, sun-bleached hair',
|
|
'schoolkid': 'an Australian school kid in 1990s public school uniform, fitted polo shirt and grey shorts, school shoes, backpack straps',
|
|
'bizdad': 'a middle-aged businessman in a fitted 1990s grey suit, white shirt, tie, black leather shoes',
|
|
'youngmum': 'a young Australian mum in 1990s high-waist jeans and a fitted striped t-shirt, white sneakers, hair in a scrunchie ponytail',
|
|
'busdriver': 'an Australian bus driver in 1990s uniform, fitted light blue short-sleeve shirt with epaulettes, navy trousers, black shoes',
|
|
'cop': 'an Australian police officer in 1990s uniform, fitted light blue shirt, navy trousers, peaked cap, black shoes',
|
|
'paperboy': 'a paperboy in 1990s clothes, fitted t-shirt, jeans rolled at the ankle, canvas sneakers, satchel strap across the chest',
|
|
}
|
|
SUFFIX = (', standing straight facing camera, arms slightly away from sides in a relaxed A-pose, '
|
|
'flat even lighting, solid green background, photorealistic, entire body in frame head to shoes')
|
|
|
|
|
|
def tok():
|
|
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')
|
|
|
|
|
|
TOK = tok()
|
|
|
|
|
|
def req(p, data=None, headers=None, raw=False):
|
|
h = {'Authorization': f'Bearer {TOK}'}
|
|
h.update(headers or {})
|
|
r = urllib.request.Request(HOST + p, data=data, headers=h)
|
|
b = urllib.request.urlopen(r, timeout=180).read()
|
|
if raw:
|
|
return b
|
|
s = b.decode('utf-8', 'replace')
|
|
return json.loads(''.join(c if c >= ' ' or c in '\t' else ' ' for c in s))
|
|
|
|
|
|
def job(operator, params=None, asset_id=None):
|
|
body = {'operator': operator, 'params': params or {}}
|
|
if asset_id:
|
|
body['asset_id'] = asset_id
|
|
return req('/api/jobs', data=json.dumps(body).encode(),
|
|
headers={'Content-Type': 'application/json'})['id']
|
|
|
|
|
|
def wait(jid, timeout=1800):
|
|
t0 = time.time()
|
|
while time.time() - t0 < timeout:
|
|
time.sleep(8)
|
|
st = req(f'/api/jobs/{jid}').get('status')
|
|
if st in ('done', 'error', 'cancelled'):
|
|
return st
|
|
return 'timeout'
|
|
|
|
|
|
def output_asset(jid, skip=None):
|
|
a = req('/api/assets?limit=60')
|
|
items = a if isinstance(a, list) else a.get('items', [])
|
|
mine = [x for x in items if x.get('parent_job') == jid and x.get('id') != skip]
|
|
return mine[0]['id'] if mine else None
|
|
|
|
|
|
def sh(cmd, timeout=1200):
|
|
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
|
|
|
|
|
def run_one(name, desc):
|
|
bank = os.path.join(BANKS, f'{name}_at_wanwalk.glb')
|
|
if os.path.exists(bank):
|
|
print(f'[{name}] SKIP (bank exists)', flush=True)
|
|
return True
|
|
print(f'[{name}] sheet…', flush=True)
|
|
j1 = job('flux_local', {'prompt': 'full body character reference of ' + desc + SUFFIX,
|
|
'steps': 6, 'width': 768, 'height': 1344})
|
|
if wait(j1, 300) != 'done':
|
|
print(f'[{name}] FLUX FAILED', flush=True); return False
|
|
sheet = output_asset(j1)
|
|
open(os.path.join(SHEETS, f'{name}.png'), 'wb').write(req(f'/api/assets/{sheet}/file', raw=True))
|
|
j2 = job('bg_remove_local', asset_id=sheet)
|
|
if wait(j2, 300) != 'done':
|
|
print(f'[{name}] KEY FAILED', flush=True); return False
|
|
cut = output_asset(j2, skip=sheet)
|
|
print(f'[{name}] mesh… (trellis)', flush=True)
|
|
j3 = job('trellis_mac', asset_id=cut)
|
|
if wait(j3, 1800) != 'done':
|
|
print(f'[{name}] TRELLIS FAILED', flush=True); return False
|
|
mesh = output_asset(j3, skip=cut)
|
|
raw = req(f'/api/assets/{mesh}/file', raw=True)
|
|
tmp_local = f'/tmp/{name}_mesh.glb'
|
|
open(tmp_local, 'wb').write(raw)
|
|
print(f'[{name}] rig+retarget… ({len(raw)//1048576}MB mesh)', flush=True)
|
|
r = sh(f'scp -q {tmp_local} {BOX}:/tmp/{name}_mesh.glb && '
|
|
f'ssh {BOX} "cd ~/Documents/MIRPAMO && ./bin/mirpamo rig /tmp/{name}_mesh.glb -o /tmp/{name}_rigged.glb && '
|
|
f'./bin/mirpamo anim /tmp/{name}_rigged.glb /tmp/wan_walk_t1.fbx -o /tmp/{name}_bank.glb" && '
|
|
f'scp -q {BOX}:/tmp/{name}_bank.glb {bank}')
|
|
if r.returncode != 0 or not os.path.exists(bank):
|
|
print(f'[{name}] RIG FAILED: {(r.stderr or r.stdout)[-200:]}', flush=True)
|
|
return False
|
|
print(f'[{name}] ✔ banked ({os.path.getsize(bank)//1048576}MB)', flush=True)
|
|
return True
|
|
|
|
|
|
def main():
|
|
only = sys.argv[1:] or list(ARCHETYPES)
|
|
ok = fail = 0
|
|
for name in only:
|
|
try:
|
|
ok += 1 if run_one(name, ARCHETYPES[name]) else 0
|
|
except Exception as e:
|
|
print(f'[{name}] EXCEPTION: {e}', flush=True)
|
|
fail += 1
|
|
print(f'=== batch done: {ok} banked, {len(only) - ok} failed/skipped-fail ===', flush=True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|