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