import { chromium } from '@playwright/test'; import fs from 'node:fs'; const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--enable-unsafe-swiftshader'] }); const context = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const page = await context.newPage(); await page.goto('http://127.0.0.1:5175/?boot=1'); await page.waitForFunction(() => window.__store?.getState().bootDone); await page.evaluate(() => window.__gameApi.startEntrance()); await page.waitForFunction(() => window.rl && !window.rl.inputLocked); await page.waitForFunction(() => window.__audioCaptureStream?.active); const script = JSON.parse(fs.readFileSync('public/scripts/reachy-microduck-conversation.json', 'utf8')); await page.evaluate(() => { const canvas = document.querySelector('canvas'); const videoStream = canvas.captureStream(30); const tracks = [...videoStream.getVideoTracks(), ...window.__audioCaptureStream.getAudioTracks()]; const recorder = new MediaRecorder(new MediaStream(tracks), { mimeType: 'video/webm;codecs=vp8,opus' }); window.__filmChunks = []; recorder.ondataavailable = (event) => { if (event.data.size) window.__filmChunks.push(event.data); }; recorder.start(250); window.__filmRecorder = recorder; }); await page.evaluate((s) => { window.rl.chaseCam = false; window.__speechMarks = []; window.__filmStart = performance.now(); window.addEventListener('reachy-speech-start', (event) => window.__speechMarks.push({ text: event.detail?.text || '', at: (performance.now() - window.__filmStart) / 1000 })); window.__gameApi.script.load(s); window.__gameApi.script.run(); }, script); await page.evaluate(async () => { const start = performance.now(); await new Promise((resolve) => { const frame = (now) => { const t = (now - start) / 1000; // One close, continuous orbit keeps both robots large in frame and // avoids the hard viewpoint jumps between emotion sections. const a = t * 0.28; const x = Math.sin(a) * 1.55; const z = Math.cos(a) * 1.55; const y = 0.72 + Math.sin(t * 0.22) * 0.04; window.rl.camera.position.set(x, y, z); window.rl.controls.target.set(0, 0.23, 0); window.rl.controls.update(); if (t < 55) requestAnimationFrame(frame); else resolve(); }; requestAnimationFrame(frame); }); }); await page.waitForTimeout(1200); const capture = await page.evaluate(() => new Promise((resolve) => { const recorder = window.__filmRecorder; recorder.onstop = () => { const blob = new Blob(window.__filmChunks, { type: 'video/webm' }); const reader = new FileReader(); reader.onload = () => resolve({ b64: reader.result.split(',')[1], marks: window.__speechMarks }); reader.readAsDataURL(blob); }; recorder.stop(); })); fs.writeFileSync('reachy-microduck-synced.webm', Buffer.from(capture.b64, 'base64')); fs.writeFileSync('reachy-microduck-speech-marks.json', JSON.stringify(capture.marks, null, 2)); await context.close(); await browser.close(); console.log('reachy-microduck-synced.webm');