import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import assert from 'node:assert/strict'; import {isDeepStrictEqual} from 'node:util'; import {login,call} from './role-permissions-live.mjs'; import {introductionPages} from '../../unreal_tran_web/src/features/training/introduction.js'; const dir=path.resolve('reports/bridge-introduction-narration-20260924'),web=path.resolve('../unreal_tran_web'); const read=name=>JSON.parse(fs.readFileSync(path.join(dir,name),'utf8')); const hash=p=>crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex'); const generation=read('generation.json'),sources=read('sources.json'),quality=read('quality.json'); assert.equal(generation.clips.length,sources.length);assert.equal(sources.length,11); for(const clip of generation.clips){ const q=quality.find(item=>item.key===clip.key&&item.sha256===clip.videoSha256); assert.ok(q?.technicalPass&&!q.needsTranscriptReview,'Media review required: '+clip.key); assert.equal(hash(path.join(web,'public',clip.videoUrl)),clip.videoSha256); assert.equal(hash(path.join(web,'public',clip.audioUrl)),clip.audioSha256); } const old=read('previous-media.json'); for(const item of old)assert.equal(hash(path.join(web,'public',item.path)),item.sha256,'Previous media changed: '+item.path); const session=await login('root'); const api=async(endpoint,body,method)=>{const r=await call('tran/v1/'+endpoint,session.accessToken,body,method);assert.equal(r.status,200,endpoint+': '+r.message);return r.data;}; const backup=(name,data)=>{const file=path.join(dir,name+'.json');if(!fs.existsSync(file))fs.writeFileSync(file,JSON.stringify(data,null,2));}; const withoutIntro=content=>{const copy=structuredClone(content);delete copy.introduction;if(copy.productionDefinition)delete copy.productionDefinition.introduction;return copy;}; const withoutClock=task=>{const copy=structuredClone(task);delete copy.serverNow;return copy;}; const records=[]; for(const id of ['86','87','88','89']){ const task=await api('teaching/assignments/'+id),clips=generation.clips.filter(c=>c.assignmentId===id); const pages=introductionPages(task.definitionSnapshot.introduction); for(const clip of clips){const p=pages.find(p=>p.id===clip.pageId);assert.equal(task.contentVersionId,clip.contentVersionId);assert.equal(p?.body,clip.body);assert.equal(p?.title,clip.title);} let project=await api('content/projects/'+task.contentProjectId);backup('before-task-'+id,task);backup('before-project-'+task.contentProjectId,project); const content=structuredClone(project.currentVersion.content),before=withoutIntro(content); // Explicit editable fields in the current production chain. The fixed task is untouched. for(const intro of [content.introduction,content.productionDefinition?.introduction]){ assert.ok(intro,'Missing authoring introduction'); for(const p of [intro.background,...intro.lectures]){const clip=clips.find(c=>c.pageId===p.id);if(!clip)continue; assert.equal(p.body.trim(),clip.body);assert.equal(p.title.trim(),clip.title); assert.ok(!p.videoAssetCode,'Do not overwrite a controlled authoring video'); assert.ok(!p.videoUrl||p.videoUrl===clip.videoUrl,'Do not overwrite an existing authoring video'); p.videoUrl=clip.videoUrl; } } if(JSON.stringify(content)!==JSON.stringify(project.currentVersion.content))project=await api('content/projects/'+task.contentProjectId, {...project.project,content,assets:project.currentVersion.assets,dependencies:project.currentVersion.dependencies,version:project.project.version,changeSummary:'背景与讲解加入 Junhao 数字教员视频;保留操作、模型和工具配置'},'PUT'); if(project.currentVersion.id!==project.project.publishedVersionId)project=await api('content/projects/'+task.contentProjectId+'/publish',{version:project.project.version}); assert.deepEqual(withoutIntro(project.currentVersion.content),before); assert.ok(isDeepStrictEqual(withoutClock(await api('teaching/assignments/'+id)),withoutClock(task)),'Fixed task must remain identical (excluding response serverNow)'); records.push({assignmentId:id,code:task.code,fixedContentVersionId:task.contentVersionId,projectId:task.contentProjectId,authoringVersionId:project.currentVersion.id,pages:clips.length}); console.log(records.at(-1)); } const manifest={release:generation.profile.revision,profile:generation.profile,clips:generation.clips.map(({key,scriptId,contentVersionId,pageId,title,body,format,videoUrl,posterUrl,audioUrl,videoSha256})=>({key,scriptId,contentVersionId,pageId,title,body,format,videoUrl,posterUrl,audioUrl,videoSha256}))}; fs.writeFileSync(path.join(web,'src/features/virtual-training-scripts/introduction-narration-manifest.js'),'// Generated from verified clips. Existing task bindings require exact frozen content.\nexport default '+JSON.stringify(manifest,null,2)+';\n'); fs.writeFileSync(path.join(dir,'activation.json'),JSON.stringify({pass:true,records,oldMediaVerified:old.length,operationsAndAssignmentsUnchanged:true,rollback:'VITE_BRIDGE_INTRO_NARRATION_RELEASE=none; authoring snapshots in before-project-*.json'},null,2)); console.log('INTRO RELEASE ACTIVATED; old media verified:',old.length);