import fs from 'node:fs'; import path from 'node:path'; import { chromium } from '@playwright/test'; const root = path.resolve('../..'); const sourceReport = path.resolve('reports/instructors-v2-20260914'); const report = path.resolve(process.env.REPORT_DIR || sourceReport); fs.mkdirSync(report,{recursive:true}); const stable = process.argv.includes('--stable'); const jobs = JSON.parse(fs.readFileSync(`${sourceReport}/jobs.json`, 'utf8')); const origin = process.env.BASE_URL || 'http://127.0.0.1:8003'; const audio = fs.readFileSync(`${root}/unreal_tran/ute2e/reports/bridge-lipsync-diagnosis-20260914/sample.wav`).toString('base64'); const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); const results = []; try { for (const job of jobs) { const id = stable ? job.stable_id : job.avatar_id; const context = await browser.newContext({ viewport: { width: 560, height: 760 } }); await context.route(`${origin}/__instructor-v2-check`, route => route.fulfill({ contentType: 'text/html', body: `

教员${job.number} · v2 语音驱动预览

` })); const page = await context.newPage(); const errors = []; page.on('pageerror', error => errors.push(error.message)); await page.goto(`${origin}/__instructor-v2-check`); const frame = page.frames().find(frame => frame.url().includes('/human/viewer/')); await frame.waitForFunction(() => window.DH_RUNTIME_READY, {}, { timeout: 120000 }); await frame.evaluate(() => { window.v2Check = { frames: 0, active: 0, maxBlend: 0 }; const original = Module._updateBlendShape; Module._updateBlendShape = function(pointer, count) { const result = original(pointer, count); const max = Math.max(...Array.from(new Float32Array(Module.HEAPU8.buffer, pointer, 12)).map(Math.abs)); window.v2Check.frames++; window.v2Check.maxBlend = Math.max(window.v2Check.maxBlend, max); if (max > .001) window.v2Check.active++; return result; }; }); await page.screenshot({ path: `${report}/${stable ? 'stable' : 'generated'}-${job.number}-idle.png` }); await page.evaluate(audioBase64 => document.querySelector('iframe').contentWindow.postMessage({ type: 'digital-human.play-audio', requestId: 'v2-validation', audioBase64, }, location.origin), audio); await page.waitForFunction(() => window.events.some(event => event.type === 'digital-human.speaking-start'), {}, { timeout: 15000 }); await frame.waitForFunction(() => window.v2Check.active > 30, {}, { timeout: 15000 }); await page.screenshot({ path: `${report}/${stable ? 'stable' : 'generated'}-${job.number}-speaking.png` }); await page.waitForFunction(() => window.events.some(event => event.type === 'digital-human.speaking-end'), {}, { timeout: 20000 }); const events = await page.evaluate(() => window.events); const driver = await frame.evaluate(() => window.v2Check); if (errors.length || events.some(event => event.type === 'digital-human.error')) throw new Error(JSON.stringify({ errors, events })); const result = { number: job.number, avatarId: id, driver, events, errors }; results.push(result); console.log(JSON.stringify(result)); fs.writeFileSync(`${report}/${stable ? 'stable' : 'generated'}-viewer-validation.json`, JSON.stringify(results, null, 2)); await context.close(); } } finally { await browser.close(); }