import fs from 'node:fs';
import crypto from 'node:crypto';
import { execFileSync } from 'node:child_process';
import assert from 'node:assert/strict';
import { login, call } from './role-permissions-live.mjs';
const dir = 'reports/bridge-digital-video-20260914';
const generation = JSON.parse(fs.readFileSync(`${dir}/generation.json`, 'utf8'));
assert.equal(generation.clips.length, 39); assert.ok(generation.clips.every(c => c.status === 'SUCCEEDED'));
const root = await login('root'), verified = [];
for (const code of ['TASK-VIRTUAL-005', 'TASK-VIRTUAL-006', 'TASK-VIRTUAL-007', 'TASK-VIRTUAL-008']) {
const binding = JSON.parse(fs.readFileSync(`${dir}/${code}-binding.json`, 'utf8'));
const task = (await call(`tran/v1/teaching/assignments/${binding.taskId}`, root.accessToken)).data;
for (const clip of generation.clips.filter(c => c.code === code)) {
const step = task.definitionSnapshot.steps.find(s => s.stepCode === clip.stepCode);
assert.equal(step.audio.narrationText, clip.text);
const asset = binding.assets.find(a => a.assetCode === step.audio.narrationVideoAssetCode); assert.ok(asset);
const response = await fetch(`http://127.0.0.1:6180${asset.downloadUrl}`, { headers: { Authorization: `Bearer ${root.accessToken}` } });
assert.equal(response.status, 200);
const bytes = Buffer.from(await response.arrayBuffer());
const sha = value => crypto.createHash('sha256').update(value).digest('hex');
assert.equal(sha(bytes), asset.sha256); assert.equal(sha(bytes), sha(fs.readFileSync(clip.previewFile)));
const probe = JSON.parse(execFileSync('ffprobe', ['-v', 'error', '-show_streams', '-show_format', '-of', 'json', clip.previewFile], { encoding: 'utf8' }));
const video = probe.streams.find(s => s.codec_type === 'video'), audio = probe.streams.find(s => s.codec_type === 'audio');
assert.equal(video.codec_name, 'h264'); assert.equal(video.width, 480); assert.equal(video.height, 640);
assert.equal(audio.codec_name, 'aac'); assert.ok(Number(probe.format.duration) > 1);
verified.push({ code, step: clip.number, assetCode: asset.assetCode, sha256: asset.sha256, duration: Number(probe.format.duration), size: bytes.length });
}
console.log('VERIFIED', code);
}
const summary = { videos: verified.length, duration: verified.reduce((n, v) => n + v.duration, 0), size: verified.reduce((n, v) => n + v.size, 0), preservedWavs: 42, verified };
fs.writeFileSync(`${dir}/verification.json`, JSON.stringify(summary, null, 2));
const escape = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
fs.writeFileSync(`${dir}/index.html`, `
四套桥梁训练数字人讲解验收四套桥梁训练 · 数字人视频讲解
39 段竖版人物视频,合计 ${summary.duration.toFixed(1)} 秒;原 39 段步骤语音和 3 段提醒完整保留。正式任务 86–89 使用发布内容版本的受控 VIDEO 资产。已核对全部视频的步骤讲解词、下载哈希及 H.264/AAC 编码。
讲解面板可切换视频/语音。切步、关闭时停止播放,视频异常自动回退原语音。历史内容版本保留,已有运行、成绩及检查点未修改。
浏览器操作验证 · 全部资源核验 · 正式运行资产/版本保护验证
${['TASK-VIRTUAL-005','TASK-VIRTUAL-006','TASK-VIRTUAL-007','TASK-VIRTUAL-008'].map(code => { const clips = generation.clips.filter(c=>c.code===code); return `${code} · ${clips.length} 段

| 步骤 | 讲解词 | 视频 |
${clips.map(c=>`| ${c.number} | ${escape(c.text)} | 播放 |
`).join('')}
`; }).join('')}`);
console.log('PASS', summary.videos, 'videos', summary.duration.toFixed(1), 'seconds');