|
- 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);
|