You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

35 line
4.5 KiB

  1. import fs from 'node:fs';
  2. import crypto from 'node:crypto';
  3. import { execFileSync } from 'node:child_process';
  4. import assert from 'node:assert/strict';
  5. import { login, call } from './role-permissions-live.mjs';
  6. const dir = 'reports/bridge-digital-video-20260914';
  7. const generation = JSON.parse(fs.readFileSync(`${dir}/generation.json`, 'utf8'));
  8. assert.equal(generation.clips.length, 39); assert.ok(generation.clips.every(c => c.status === 'SUCCEEDED'));
  9. const root = await login('root'), verified = [];
  10. for (const code of ['TASK-VIRTUAL-005', 'TASK-VIRTUAL-006', 'TASK-VIRTUAL-007', 'TASK-VIRTUAL-008']) {
  11. const binding = JSON.parse(fs.readFileSync(`${dir}/${code}-binding.json`, 'utf8'));
  12. const task = (await call(`tran/v1/teaching/assignments/${binding.taskId}`, root.accessToken)).data;
  13. for (const clip of generation.clips.filter(c => c.code === code)) {
  14. const step = task.definitionSnapshot.steps.find(s => s.stepCode === clip.stepCode);
  15. assert.equal(step.audio.narrationText, clip.text);
  16. const asset = binding.assets.find(a => a.assetCode === step.audio.narrationVideoAssetCode); assert.ok(asset);
  17. const response = await fetch(`http://127.0.0.1:6180${asset.downloadUrl}`, { headers: { Authorization: `Bearer ${root.accessToken}` } });
  18. assert.equal(response.status, 200);
  19. const bytes = Buffer.from(await response.arrayBuffer());
  20. const sha = value => crypto.createHash('sha256').update(value).digest('hex');
  21. assert.equal(sha(bytes), asset.sha256); assert.equal(sha(bytes), sha(fs.readFileSync(clip.previewFile)));
  22. const probe = JSON.parse(execFileSync('ffprobe', ['-v', 'error', '-show_streams', '-show_format', '-of', 'json', clip.previewFile], { encoding: 'utf8' }));
  23. const video = probe.streams.find(s => s.codec_type === 'video'), audio = probe.streams.find(s => s.codec_type === 'audio');
  24. assert.equal(video.codec_name, 'h264'); assert.equal(video.width, 480); assert.equal(video.height, 640);
  25. assert.equal(audio.codec_name, 'aac'); assert.ok(Number(probe.format.duration) > 1);
  26. verified.push({ code, step: clip.number, assetCode: asset.assetCode, sha256: asset.sha256, duration: Number(probe.format.duration), size: bytes.length });
  27. }
  28. console.log('VERIFIED', code);
  29. }
  30. 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 };
  31. fs.writeFileSync(`${dir}/verification.json`, JSON.stringify(summary, null, 2));
  32. const escape = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
  33. fs.writeFileSync(`${dir}/index.html`, `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>四套桥梁训练数字人讲解验收</title><style>body{font:16px/1.65 system-ui;margin:32px;color:#143e36;background:#f3f8f6}main{max-width:1200px;margin:auto}section{background:white;border:1px solid #c9ddd6;border-radius:12px;padding:24px;margin:20px 0}video{width:180px;max-height:250px;vertical-align:top}img{max-width:900px;width:75%;vertical-align:top;margin-left:18px}table{border-collapse:collapse;width:100%}td,th{border:1px solid #d6e4df;padding:8px;text-align:left}a{color:#08795c}</style><main><h1>四套桥梁训练 · 数字人视频讲解</h1><p>39 段竖版人物视频,合计 ${summary.duration.toFixed(1)} 秒;原 39 段步骤语音和 3 段提醒完整保留。正式任务 86–89 使用发布内容版本的受控 VIDEO 资产。已核对全部视频的步骤讲解词、下载哈希及 H.264/AAC 编码。</p><p>讲解面板可切换视频/语音。切步、关闭时停止播放,视频异常自动回退原语音。历史内容版本保留,已有运行、成绩及检查点未修改。</p><p><a href="live-result.json">浏览器操作验证</a> · <a href="verification.json">全部资源核验</a> · <a href="run-assets.json">正式运行资产/版本保护验证</a></p>${['TASK-VIRTUAL-005','TASK-VIRTUAL-006','TASK-VIRTUAL-007','TASK-VIRTUAL-008'].map(code => { const clips = generation.clips.filter(c=>c.code===code); return `<section><h2>${code} · ${clips.length} 段</h2><video controls preload="metadata" src="${code}-step-01.mp4"></video><img src="${code}-video.png"><table><tr><th>步骤</th><th>讲解词</th><th>视频</th></tr>${clips.map(c=>`<tr><td>${c.number}</td><td>${escape(c.text)}</td><td><a href="${code}-step-${String(c.number).padStart(2,'0')}.mp4">播放</a></td></tr>`).join('')}</table></section>`; }).join('')}</main></html>`);
  34. console.log('PASS', summary.videos, 'videos', summary.duration.toFixed(1), 'seconds');