#!/usr/bin/env python3
"""Use four actual mesh views to record nose/exhaust evidence, then jev chooses the stated front."""
import base64,concurrent.futures,hashlib,json,subprocess,sys
from pathlib import Path
from PIL import Image,ImageDraw
ROOT=Path(__file__).resolve().parents[1];sys.path[:0]=[str(ROOT/'lib'),str(ROOT/'tools'),str(Path.home()/'forge-hub/agent')]
import card,ingest_photo_object as imp
from typesafe_client import JevClient,choice
import argparse
p=argparse.ArgumentParser();p.add_argument('--frames',default='/tmp/wlib-frames');p.add_argument('--apply',action='store_true');a=p.parse_args();frames=Path(a.frames)
idx=json.loads((ROOT/'index.json').read_text());assets=[x for x in idx['assets'] if any(t in x['id'] for t in ['/trellis2-','/pixal3d-'])]
for x in assets:
    sheet=Image.new('RGB',(768,824),'white');draw=ImageDraw.Draw(sheet)
    for i,label in enumerate(['+Z','+X','-Z','-X']):
        x0,y0=(i%2)*384,(i//2)*412;sheet.paste(Image.open(frames/(x['glb']+f'-{i}.jpg')),(x0,y0+28));draw.text((x0+16,y0+8),'Camera on '+label+' looking at object',fill='black')
    path=ROOT/'media'/x['glb']/'frame-audit.jpg';path.parent.mkdir(parents=True,exist_ok=True);sheet.save(path,quality=87)

def describe(batch):
    content=[]
    for i,x in enumerate(batch):
        content.extend([{'type':'text','text':f'Object {i+1}'},{'type':'image_url','image_url':{'url':'data:image/jpeg;base64,'+base64.b64encode((ROOT/'media'/x['glb']/'frame-audit.jpg').read_bytes()).decode()}}])
    content.append({'type':'text','text':'Each contact sheet shows ONE 3D spacecraft from four labelled camera sides. Write a short description of the four views, especially which labelled camera side clearly shows the forward nose/cockpit and which shows the aft engines/exhausts. Use the printed labels +Z,+X,-Z,-X, not left/right on the contact sheet. Describe symmetry or ambiguity plainly. Do not invent hidden details. Return JSON {"items":[{"n":1,"description":"..."}]}. This is visual description only; do not decide a rotation.'})
    req={'model':'forge:build-gemini-4','response_format':{'type':'json_object'},'messages':[{'role':'user','content':content}]}
    r=subprocess.run([str(Path.home()/'forge-build/spatial-rebuild-20261013/tools/vlm.sh')],input=json.dumps(req),text=True,capture_output=True,timeout=360);text=json.loads(r.stdout)['text'].strip().removeprefix('```json').removesuffix('```').strip();items=json.loads(text)['items'];rows={int(v['n']):v['description'] for v in items};return [(x,rows[i+1]) for i,x in enumerate(batch)]
rows=[]
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool:
    for result in pool.map(describe,[assets[i:i+8] for i in range(0,len(assets),8)]):rows.extend(result)
records=[]
for x,desc in rows:
    r=JevClient().ask(desc,{'front':choice('Which camera label does the description explicitly identify as showing the forward nose or cockpit, rather than the aft engine exhausts?',{'+Z':'Camera on +Z sees the front nose','+X':'Camera on +X sees the front nose','-Z':'Camera on -Z sees the front nose','-X':'Camera on -X sees the front nose','symmetric':'Object has rotational symmetry and no unique front','unknown':'Description does not identify the front or is contradictory'})})
    answer=r.answers['front'];front=answer['choice'];confidence=answer['confidence'];yaw={'+Z':0,'+X':-90,'-Z':180,'-X':90,'symmetric':0}.get(front)
    rec={'id':x['id'],'old_release':x['release'],'description':desc,'front':front,'confidence':confidence,'yaw':yaw,'evidence':f"media/{x['glb']}/frame-audit.jpg"};records.append(rec)
    if a.apply and yaw is not None and confidence>=.8:
        c=json.loads((ROOT/x['card']).read_text());path=ROOT/'store/blobs'/(x['glb']+'.glb')
        if yaw:
            binary=imp.normalize(path,1,yaw);sha=hashlib.sha256(binary).hexdigest();dest=ROOT/'store/blobs'/(sha+'.glb')
            if not dest.exists():dest.write_bytes(binary)
            import glb
            m=glb.measure(dest);c['version']+=1;c['files']['glb']='sha256:'+sha;c['render'].update({k:m[k] for k in ['bytes','tris','materials','draw_calls','gpu_mb_decoded','textures','codec']});c['size']['measured_m']={'w':m['size_xyz'][0],'h':m['size_xyz'][1],'l':m['size_xyz'][2]};c['bounds'].update(m['bounds']);c['bounds']['footprint'].update(minX=m['bounds']['min'][0],maxX=m['bounds']['max'][0],minZ=m['bounds']['min'][2],maxZ=m['bounds']['max'][2]);c['bounds']['collider']['parts']=[{'type':'box','x':0,'y':m['size_xyz'][1]/2,'z':0,'hx':m['size_xyz'][0]/2,'hy':m['size_xyz'][1]/2,'hz':m['size_xyz'][2]/2}]
        c['frame'].update(contract='props/+Z-front-ground',unit='m');c['provenance']['front_evidence']={'source':'authored','method':'Gemini 4 describes four mesh views; jev selects nose-facing side','front_before':front,'confidence':confidence,'contact_sheet':rec['evidence'],'description':desc};c['release']=card.release_hash(c);card.write(ROOT,c)
    print(x['id'],front,confidence,'yaw',yaw,flush=True)
(ROOT/'frame-audit.json').write_text(json.dumps({'vision_calls':(len(assets)+7)//8,'jev_calls':len(records),'assets':records},indent=1))
