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(); }