|
- // Verify project/build copies and browser delivery without requiring avatar services.
- import assert from 'node:assert/strict';
- import fs from 'node:fs';
- import { createHash } from 'node:crypto';
- import { fileURLToPath } from 'node:url';
- import { spawnSync } from 'node:child_process';
- import videos from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-video-manifest.js';
- import speech from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-manifest.js';
-
- const root = new URL('../../', import.meta.url);
- const report = new URL('ute2e/reports/bridge-digital-video-static-20260915/', root);
- const sha = data => createHash('sha256').update(data).digest('hex');
- const generated = JSON.parse(fs.readFileSync(new URL('generation.json', report), 'utf8'));
- const original = JSON.parse(fs.readFileSync(new URL('ute2e/reports/bridge-digital-video-20260914/generation.json', root), 'utf8'));
- const clips = videos.scripts.flatMap(script => script.steps);
- assert.equal(clips.length, 39);
- const preview = new URL('index.html', report);
- const previewLinks = [...fs.readFileSync(preview, 'utf8').matchAll(/<video[^>]+src="([^"]+)"/g)];
- assert.equal(previewLinks.length, 39);
- for (const [, link] of previewLinks) assert.ok(fs.existsSync(new URL(link, preview)), `Broken preview ${link}`);
- const origins = ['http://127.0.0.1:6180', process.env.LAN_URL || 'http://192.168.31.168:6180'];
- const checks = [];
- for (const origin of origins) {
- for (const clip of clips) {
- const response = await fetch(origin + clip.url, { signal: AbortSignal.timeout(30000) });
- assert.equal(response.status, 200, `${origin} ${clip.url}`);
- assert.match(response.headers.get('content-type'), /video\/mp4/);
- assert.equal(sha(Buffer.from(await response.arrayBuffer())), clip.sha256);
- }
- const range = await fetch(origin + clips[0].url, { headers: { Range: 'bytes=0-1023' }, signal: AbortSignal.timeout(30000) });
- assert.equal(range.status, 206);
- assert.equal((await range.arrayBuffer()).byteLength, 1024);
- checks.push({ origin, playableUrls: clips.length, exactFiles: true, rangeRequests: true });
- }
- const audioChecks = [];
- for (const clip of clips) {
- const file = fs.readFileSync(new URL(`unreal_tran_web/dist${clip.url}`, root));
- assert.equal(sha(file), clip.sha256, `Missing or stale build file ${clip.url}`);
- const volume = spawnSync(process.env.FFMPEG_BINARY || 'ffmpeg', ['-hide_banner', '-nostdin',
- '-i', fileURLToPath(new URL(`unreal_tran_web/public${clip.url}`, root)), '-map', '0:a:0',
- '-af', 'volumedetect', '-f', 'null', '-'], { encoding: 'utf8', windowsHide: true, timeout: 30000 });
- assert.equal(volume.status, 0);
- const mean = Number(volume.stderr.match(/mean_volume:\s*(-?\d+(?:\.\d+)?)\s*dB/)?.[1]);
- assert.ok(Number.isFinite(mean) && mean > -70, `Silent narration ${clip.url}`);
- audioChecks.push({ url: clip.url, meanVolumeDb: mean });
- }
- for (const source of original.clips) {
- const record = generated.clips.find(clip => clip.key === source.key);
- assert.equal(sha(fs.readFileSync(new URL(`ute2e/${source.previewFile}`, root))), record.sourceVideoSha256);
- }
- let preservedWavs = 0;
- for (const script of speech.scripts) for (const clip of script.steps) {
- for (const [url, hash] of [[clip.url, clip.stepSha256], [clip.safetyUrl, clip.safetySha256]]) {
- if (!url) continue;
- assert.equal(sha(fs.readFileSync(new URL(`unreal_tran_web/public${url}`, root))), hash);
- preservedWavs++;
- }
- }
- const result = { clips: clips.length, checks, buildFilesVerified: clips.length,
- audibleNarrations: audioChecks,
- originalVideosPreserved: original.clips.length, originalWavsPreserved: preservedWavs,
- totalDuration: Number(clips.reduce((sum, clip) => sum + clip.duration, 0).toFixed(2)),
- totalBytes: clips.reduce((sum, clip) => sum + fs.statSync(new URL(`unreal_tran_web/public${clip.url}`, root)).size, 0),
- taskDataMutated: false };
- fs.writeFileSync(new URL('delivery-validation.json', report), JSON.stringify(result, null, 2));
- console.log(JSON.stringify({ ...result, audibleNarrations: audioChecks.length, report: fileURLToPath(new URL('delivery-validation.json', report)) }));
|