import fs from 'node:fs'; import path from 'node:path'; import assert from 'node:assert/strict'; import crypto from 'node:crypto'; const web=path.resolve('../unreal_tran_web'),dir=path.resolve('reports/bridge-narration-junhao-20260921'); const source=path.join(web,'src/features/virtual-training-scripts'); const audio=JSON.parse(fs.readFileSync(path.join(dir,'audio-manifest.json'),'utf8')); const video=JSON.parse(fs.readFileSync(path.join(dir,'video-manifest.json'),'utf8')); const quality=JSON.parse(fs.readFileSync(path.join(dir,'quality.json'),'utf8')); const hash=file=>crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); assert.equal(quality.length,39);assert.ok(quality.every(c=>c.technicalPass)); for(const old of JSON.parse(fs.readFileSync(path.join(dir,'previous-media.json'),'utf8')))assert.equal(hash(web+'/public'+old.url),old.sha256); assert.equal(audio.scripts.flatMap(p=>p.steps).length,39); for(const pack of audio.scripts)for(const clip of pack.steps){ const v=video.scripts.find(p=>p.code===pack.code)?.steps.find(s=>s.stepCode===clip.stepCode); assert.equal(v.text,clip.text);assert.equal(v.expectedTitle,clip.expectedTitle);assert.equal(v.expectedInstruction,clip.expectedInstruction); assert.equal(hash(web+'/public'+clip.url),clip.stepSha256);assert.equal(hash(web+'/public'+v.url),v.sha256); assert.equal(quality.find(q=>q.key===`${pack.code}-step-${String(clip.number).padStart(2,'0')}`).sha256,v.sha256); if(clip.safetyUrl){assert.match(clip.safetyUrl,/junhao-v2-20260921/);assert.equal(hash(web+'/public'+clip.safetyUrl),clip.safetySha256);} } for(const [name,data]of [['narration-junhao-20260921.js',audio],['narration-video-junhao-20260921.js',video]]) fs.writeFileSync(path.join(source,name),'// Generated by bridge-narration-current-voice.py; original release remains unchanged.\nexport default '+JSON.stringify(data,null,2)+';\n'); fs.writeFileSync(path.join(source,'narration-release.js'),`import previousAudio from './narration-manifest.js'; import previousVideo from './narration-video-manifest.js'; import junhaoAudio from './narration-junhao-20260921.js'; import junhaoVideo from './narration-video-junhao-20260921.js'; // Build-time switch. Preserve both generations so rollback never regenerates media. export function selectNarrationRelease(name) { if (name === 'previous') return { manifest: previousAudio, videos: previousVideo }; if (name === 'junhao-v2-20260921') return { manifest: junhaoAudio, videos: junhaoVideo }; throw new Error('未知的桥梁讲解资源版本:' + name); } export const activeRelease = import.meta.env?.VITE_BRIDGE_NARRATION_RELEASE || 'junhao-v2-20260921'; export const { manifest, videos } = selectNarrationRelease(activeRelease); `); const filename=path.join(source,'narration.js'); const original=fs.readFileSync(filename,'utf8'); const replacement=original.replace("import manifest from './narration-manifest.js';\nimport videos from './narration-video-manifest.js';","import { manifest, videos } from './narration-release.js';"); assert.ok(replacement.includes("import { manifest, videos } from './narration-release.js';")); fs.writeFileSync(filename,replacement); fs.writeFileSync(path.join(dir,'activation.json'),JSON.stringify({release:'junhao-v2-20260921',oldMediaCount:80,oldMediaUnchanged:true,clips:39,qualityPassed:true,taskDefinitionsChanged:false},null,2)); console.log('Activated 39 clips; previous 80 media files preserved.');