File size: 7,044 Bytes
b21cfae | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | """Real Chromium geometry, interpolation, playback cue and live Luna checks.
uv run --with playwright --python /path/to/python expression_checks.py [--live]
Synthetic typed requests only, no private microphone/camera recordings.
"""
import argparse,asyncio,json
from pathlib import Path
from playwright.async_api import async_playwright
async def run(args):
logs=Path(__file__).parent/'logs';logs.mkdir(exist_ok=True)
async with async_playwright() as p:
browser=await p.chromium.launch(executable_path='/usr/bin/google-chrome',headless=True,
args=['--no-sandbox','--autoplay-policy=no-user-gesture-required'])
page=await browser.new_page(viewport={'width':1440,'height':1100});errors=[]
page.on('pageerror',lambda e:errors.append(str(e)))
try:
await page.goto(args.url+'/expressions/');await page.wait_for_function('window.robotGallery')
result=await page.evaluate('''async()=>{
const {profiles,pose,mix,geometry,KEYPOINT_IDS}=window.robotGallery;
const assert=(yes,msg)=>{if(!yes)throw Error(msg)};
assert(profiles.length===40,'Forty profiles missing');assert(KEYPOINT_IDS.length===101,'Anchor count');
assert(new Set(KEYPOINT_IDS).size===101,'Duplicate anchors');
const points=profiles.map(p=>geometry(pose(p.slug,1,0)).points.flat());
assert(points.every(p=>p.length===202&&p.every(Number.isFinite)),'Invalid coordinates');
assert(new Set(points.map(p=>JSON.stringify(p))).size===40,'Identical archetypes');
let pairs=0,minDistance=Infinity;
for(let i=0;i<40;i++)for(let j=i+1;j<40;j++){
const distance=Math.sqrt(points[i].reduce((a,v,k)=>a+(v-points[j][k])**2,0)/202);minDistance=Math.min(minDistance,distance);
for(const t of [.25,.5,.75])assert(geometry(mix(pose(profiles[i].slug),pose(profiles[j].slug),t)).points.flat().every(Number.isFinite),'Invalid mixture');
pairs++;
}
const {ActingTimeline}=await import('/static/emotions.js');const tl=new ActingTimeline();
tl.set(7,{source:'test',sentences:[{emotion:'anger',start_s:0,end_s:1},{emotion:'relief',start_s:1,end_s:2}]});
assert(tl.at({id:7,sample:12000}).emotion==='anger','Early cue');
assert(tl.at({id:7,sample:36000}).emotion==='relief','Late cue');
assert(tl.at(null)===null,'Buffer gap advanced plan');tl.enabled=false;assert(tl.at({id:7,sample:12000})===null,'Toggle ignored');
tl.enabled=true;tl.clear();assert(tl.at({id:7,sample:12000})===null,'Interrupted cue survived');
return {profiles:40,unique_archetype_geometries:40,keypoints:101,mixture_pairs:pairs,mixture_positions:3,
min_pair_rms_pixels:minDistance,playback_cues:true,buffer_gap_holds:true,cancellation_clears:true,
human_recognition_accuracy:null};}''')
await page.screenshot(path=str(logs/'expressions-gallery.png'),full_page=True)
await page.select_option('#a','anger');await page.select_option('#b','surprise');await page.fill('#blend','0.5')
await page.check('#points');await page.wait_for_timeout(700)
result['interactive_blend_and_anchors']=await page.evaluate('''({blend:document.querySelector('#blend-value').textContent,
head:document.querySelectorAll('#keypoints circle').length,body:document.querySelectorAll('#body-keypoints circle').length,
visible:document.querySelector('#body-keypoints').getAttribute('opacity')==='1'})''')
assert result['interactive_blend_and_anchors']=={'blend':'50%','head':89,'body':12,'visible':True}
await page.locator('.expression-lab').screenshot(path=str(logs/'expression-blend-keypoints.png'));await page.uncheck('#points')
for emotion in ['anger','sadness','surprise','teasing','disgust','contentment']:
await page.select_option('#a',emotion);await page.fill('#blend','0');await page.wait_for_timeout(700)
await page.locator('.expression-lab').screenshot(path=str(logs/('expression-'+emotion+'.png')))
await page.set_viewport_size({'width':390,'height':844})
result['gallery_mobile_no_overflow']=await page.evaluate('document.documentElement.scrollWidth<=innerWidth')
await page.goto(args.url+'/');await page.wait_for_function('window.robotFace&&window.robo')
result['no_heart_or_cross_in_robot']=await page.evaluate('''!document.querySelector('#avatar svg').outerHTML.match(/heart|cross|M535 565/)''')
await page.select_option('#expression','anger');await page.wait_for_timeout(500)
result['manual_preview']=await page.locator('#acting-status').text_content()
await page.select_option('#expression','auto')
await page.screenshot(path=str(logs/'robby-expressive-mobile.png'),full_page=True)
if args.live:
await page.fill('#message','Give me two very short sentences: first very angry, then relieved. Make the change clear in your acting directions.');await page.click('button[type=submit]')
await page.wait_for_function('window.robotFace.timeline.plans.size>0',timeout=90000)
result['luna_plans']=await page.evaluate('Array.from(window.robotFace.timeline.plans.entries())')
await page.wait_for_function('window.robo.metrics.chunks>0',timeout=90000)
await page.wait_for_function('window.robo.clock.position()!==null&&document.querySelector("#acting-status").textContent.includes("Luna SCRIPT")',timeout=30000)
result['live_first_playback']=await page.evaluate('({chunks:robo.metrics.chunks,position:robo.clock.position(),face:document.querySelector("#acting-status").textContent})')
await page.wait_for_function('document.querySelector("#acting-status").textContent.includes("sentence 2")',timeout=30000)
result['live_second_sentence']=await page.locator('#acting-status').text_content()
await page.screenshot(path=str(logs/'robby-luna-relief.png'))
await page.click('#interrupt');await page.wait_for_timeout(150)
result['live_interruption_clears']=await page.evaluate('robotFace.timeline.plans.size===0&&robo.clock.position()===null')
await page.evaluate('robo.stop()')
result['page_errors']=errors
assert not errors and result['gallery_mobile_no_overflow'] and result['no_heart_or_cross_in_robot'],result
(logs/('expression-checks-live.json' if args.live else 'expression-checks.json')).write_text(json.dumps(result,indent=2));print(json.dumps(result,indent=2))
finally:await browser.close()
if __name__=='__main__':
parser=argparse.ArgumentParser();parser.add_argument('--url',default='http://127.0.0.1:8793');parser.add_argument('--live',action='store_true');asyncio.run(run(parser.parse_args()))
|