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.
 
 
 
 

55 lines
5.1 KiB

  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import crypto from 'node:crypto';
  4. import assert from 'node:assert/strict';
  5. import {isDeepStrictEqual} from 'node:util';
  6. import {login,call} from './role-permissions-live.mjs';
  7. import {introductionPages} from '../../unreal_tran_web/src/features/training/introduction.js';
  8. const dir=path.resolve('reports/bridge-introduction-narration-20260924'),web=path.resolve('../unreal_tran_web');
  9. const read=name=>JSON.parse(fs.readFileSync(path.join(dir,name),'utf8'));
  10. const hash=p=>crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
  11. const generation=read('generation.json'),sources=read('sources.json'),quality=read('quality.json');
  12. assert.equal(generation.clips.length,sources.length);assert.equal(sources.length,11);
  13. for(const clip of generation.clips){
  14. const q=quality.find(item=>item.key===clip.key&&item.sha256===clip.videoSha256);
  15. assert.ok(q?.technicalPass&&!q.needsTranscriptReview,'Media review required: '+clip.key);
  16. assert.equal(hash(path.join(web,'public',clip.videoUrl)),clip.videoSha256);
  17. assert.equal(hash(path.join(web,'public',clip.audioUrl)),clip.audioSha256);
  18. }
  19. const old=read('previous-media.json');
  20. for(const item of old)assert.equal(hash(path.join(web,'public',item.path)),item.sha256,'Previous media changed: '+item.path);
  21. const session=await login('root');
  22. 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;};
  23. const backup=(name,data)=>{const file=path.join(dir,name+'.json');if(!fs.existsSync(file))fs.writeFileSync(file,JSON.stringify(data,null,2));};
  24. const withoutIntro=content=>{const copy=structuredClone(content);delete copy.introduction;if(copy.productionDefinition)delete copy.productionDefinition.introduction;return copy;};
  25. const withoutClock=task=>{const copy=structuredClone(task);delete copy.serverNow;return copy;};
  26. const records=[];
  27. for(const id of ['86','87','88','89']){
  28. const task=await api('teaching/assignments/'+id),clips=generation.clips.filter(c=>c.assignmentId===id);
  29. const pages=introductionPages(task.definitionSnapshot.introduction);
  30. 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);}
  31. let project=await api('content/projects/'+task.contentProjectId);backup('before-task-'+id,task);backup('before-project-'+task.contentProjectId,project);
  32. const content=structuredClone(project.currentVersion.content),before=withoutIntro(content);
  33. // Explicit editable fields in the current production chain. The fixed task is untouched.
  34. for(const intro of [content.introduction,content.productionDefinition?.introduction]){
  35. assert.ok(intro,'Missing authoring introduction');
  36. for(const p of [intro.background,...intro.lectures]){const clip=clips.find(c=>c.pageId===p.id);if(!clip)continue;
  37. assert.equal(p.body.trim(),clip.body);assert.equal(p.title.trim(),clip.title);
  38. assert.ok(!p.videoAssetCode,'Do not overwrite a controlled authoring video');
  39. assert.ok(!p.videoUrl||p.videoUrl===clip.videoUrl,'Do not overwrite an existing authoring video');
  40. p.videoUrl=clip.videoUrl;
  41. }
  42. }
  43. if(JSON.stringify(content)!==JSON.stringify(project.currentVersion.content))project=await api('content/projects/'+task.contentProjectId,
  44. {...project.project,content,assets:project.currentVersion.assets,dependencies:project.currentVersion.dependencies,version:project.project.version,changeSummary:'背景与讲解加入 Junhao 数字教员视频;保留操作、模型和工具配置'},'PUT');
  45. if(project.currentVersion.id!==project.project.publishedVersionId)project=await api('content/projects/'+task.contentProjectId+'/publish',{version:project.project.version});
  46. assert.deepEqual(withoutIntro(project.currentVersion.content),before);
  47. assert.ok(isDeepStrictEqual(withoutClock(await api('teaching/assignments/'+id)),withoutClock(task)),'Fixed task must remain identical (excluding response serverNow)');
  48. records.push({assignmentId:id,code:task.code,fixedContentVersionId:task.contentVersionId,projectId:task.contentProjectId,authoringVersionId:project.currentVersion.id,pages:clips.length});
  49. console.log(records.at(-1));
  50. }
  51. 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}))};
  52. 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');
  53. 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));
  54. console.log('INTRO RELEASE ACTIVATED; old media verified:',old.length);