import fs from 'node:fs'; import assert from 'node:assert/strict'; import { login, call } from './role-permissions-live.mjs'; import manifest from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-manifest.js'; const dir = 'reports/bridge-digital-video-20260914'; const generated = JSON.parse(fs.readFileSync(`${dir}/generation.json`, 'utf8')); const session = await login('root'); const api = async (path, body, method) => { const r = await call(`tran/v1/${path}`, session.accessToken, body, method); assert.equal(r.status, 200, `${path}: ${r.message}`); return r.data; }; const assignments = (await api('teaching/assignments?channel=VIRTUAL&assignmentKind=TRAINING&size=100')).records; const codes = process.argv.slice(2); for (const pack of manifest.scripts.filter(p => !codes.length || codes.includes(p.code))) { const clips = generated.clips.filter(c => c.code === pack.code); assert.equal(clips.length, pack.steps.length); assert.ok(clips.every(c => c.status === 'SUCCEEDED')); let task = assignments.find(t => t.code === pack.code); let detail = await api(`content/projects/${task.contentProjectId}`); const backup = `${dir}/${pack.code}-before.json`; if (!fs.existsSync(backup)) fs.writeFileSync(backup, JSON.stringify({ task, detail, runs: await api(`teaching/runs?assignmentId=${task.id}&size=100`) }, null, 2)); const content = structuredClone(detail.currentVersion.content), assets = [...detail.currentVersion.assets]; let changed = false; for (const step of content.steps) { const clip = clips.find(c => c.stepCode === (step.stepCode || step.id)); assert.ok(clip); const existing = assets.find(a => a.code === step.audio?.narrationVideoAssetCode && String(a.metadata?.sourceVideoId) === clip.videoId); if (existing) continue; const asset = await api(`content/projects/${task.contentProjectId}/digital-human/videos/${clip.videoId}/import`, { version: detail.project.version, name: clip.title, appId: clip.appId }); assets.push(asset); step.audio = { ...step.audio, narrationSource: 'digital-generate', narrationText: clip.text, narrationUrl: '', narrationAssetCode: '', narrationVideoAssetCode: asset.code, digitalHuman: { enabled: true, mode: 'video', videoSource: 'library', videoId: clip.videoId, videoName: clip.title, appId: clip.appId, videoUrl: '', generatedVideoUrl: '', generationStatus: 'generated' } }; changed = true; console.log('IMPORTED', pack.code, clip.number, asset.code); } if (changed) { detail = await api(`content/projects/${task.contentProjectId}`, { ...detail.project, content, dependencies: detail.currentVersion.dependencies, assets, changeSummary: '39段数字人视频讲解;原语音保留可切换' }, 'PUT'); } if (detail.project.currentVersionStatus !== 'PUBLISHED') { await api(`content/projects/${task.contentProjectId}/publish`, { version: detail.project.version }); detail = await api(`content/projects/${task.contentProjectId}`); } if (task.contentVersionId !== detail.project.publishedVersionId) { task = await api(`teaching/assignments/${task.id}/narration`, { version: task.version, contentVersionId: detail.project.publishedVersionId, reason: '补充逐步数字人讲解视频,保留原语音,训练操作和评分不变' }, 'PUT'); } const original = JSON.parse(fs.readFileSync(backup, 'utf8')); const runs = await api(`teaching/runs?assignmentId=${task.id}&size=100`); const stableRuns = value => JSON.parse(JSON.stringify(value, (key, item) => key === 'serverNow' ? undefined : item)); assert.deepEqual(stableRuns(runs), stableRuns(original.runs), 'Existing training runs must not change'); const result = { code: pack.code, taskId: task.id, contentVersionId: task.contentVersionId, clips: task.definitionSnapshot.steps.map(s => ({ stepCode: s.stepCode, videoAssetCode: s.audio.narrationVideoAssetCode })), runsUnchanged: true, assets: await api(`teaching/assignments/${task.id}/assets`) }; fs.writeFileSync(`${dir}/${pack.code}-binding.json`, JSON.stringify(result, null, 2)); console.log('BOUND', pack.code, task.contentVersionId, result.clips.length); }