#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawnSync } = require('child_process'); const { VIDEO_RESOURCE_HEADER } = require('./create-video-intake-pack'); const root = path.resolve(__dirname, '..'); function main() { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-video-resource-readiness-')); const passCsv = path.join(tmp, 'video-pass.csv'); const failCsv = path.join(tmp, 'video-fail.csv'); const passOutput = path.join(tmp, 'pass-output'); const failOutput = path.join(tmp, 'fail-output'); writeCsv(passCsv, [ VIDEO_RESOURCE_HEADER, row({ briefId: 'brief-001', role: '参考视频', creatorName: '参考博主', videoUrl: 'https://example.com/reference.mp4', coverUrl: 'https://example.com/reference-cover.jpg', asrText: '真实 ASR 文本,描述人群、场景、口播结构和表达方式。', frameUrls: 'https://example.com/reference-frame.jpg', isReal: '是' }), row({ briefId: 'brief-001', role: '候选视频', creatorName: '候选博主', videoUrl: 'https://example.com/candidate.mp4', coverUrl: 'https://example.com/candidate-cover.jpg', asrText: '候选视频 ASR 文本,能够辅助判断调性是否接近。', frameUrls: '', isReal: '是' }) ]); writeCsv(failCsv, [ VIDEO_RESOURCE_HEADER, row({ briefId: 'brief-001', role: '参考视频', creatorName: '参考博主', videoUrl: '', coverUrl: '', asrText: '', frameUrls: '', isReal: '否' }) ]); runAudit(passCsv, passOutput, true); runAudit(failCsv, failOutput, false); const passSummary = JSON.parse(fs.readFileSync(path.join(passOutput, 'video-resource-readiness-summary.json'), 'utf8')); const failSummary = JSON.parse(fs.readFileSync(path.join(failOutput, 'video-resource-readiness-summary.json'), 'utf8')); const report = fs.readFileSync(path.join(passOutput, 'video-resource-readiness-report.md'), 'utf8').replace(/^\uFEFF/, ''); const failReport = fs.readFileSync(path.join(failOutput, 'video-resource-readiness-report.md'), 'utf8').replace(/^\uFEFF/, ''); const failRepairCsv = path.join(failOutput, 'video-resource-repair-actions.csv'); assert(passSummary.acceptance.readyForVideoAbPreflight === true, 'complete video resources should pass preflight readiness'); assert(Array.isArray(passSummary.repairActions) && passSummary.repairActions.length === 0, 'passing sample should have no repair actions'); assert(passSummary.counts.realReferenceRows === 1, 'passing sample should include one real reference row'); assert(passSummary.counts.realCandidateRows === 1, 'passing sample should include one real candidate row'); assert(passSummary.counts.videoUrlRows === 2, 'passing sample should include video URLs'); assert(failSummary.acceptance.readyForVideoAbPreflight === false, 'missing video resources should fail readiness'); assert(Array.isArray(failSummary.repairActions) && failSummary.repairActions.length === failSummary.failureCount, 'failing sample should expose repair action for each issue'); assert(fs.existsSync(failRepairCsv), 'failing audit should write repair actions CSV'); assert(failSummary.issues.some(item => item.type === 'missing-real-reference'), 'failure should mention missing real reference resource'); assert(failSummary.issues.some(item => item.type === 'missing-video-url'), 'failure should mention missing video URL'); assert(report.includes('# 视频资源就绪审计'), 'report should be Chinese'); assert(failReport.includes('## 修复清单'), 'failing report should include repair action section'); assert(fs.readFileSync(failRepairCsv, 'utf8').includes('修复动作'), 'video repair action CSV should include action column'); assert(!/(sk-[A-Za-z0-9_-]{20,}|r:[A-Za-z0-9]{20,}|Authorization\s*[:=]\s*Bearer)/i.test(report), 'report should not contain secrets'); console.log(JSON.stringify({ ok: true, passOutput, failOutput, passFailureCount: passSummary.failureCount, failFailureCount: failSummary.failureCount }, null, 2)); } function runAudit(input, output, shouldPass) { const result = spawnSync(process.execPath, [ path.join(root, 'scripts', 'video-resource-readiness-audit.js'), '--input', input, '--output', output, '--strict' ], { cwd: root, encoding: 'utf8' }); if (shouldPass && result.status !== 0) { process.stderr.write(result.stderr || result.stdout || ''); throw new Error('passing video resource readiness audit should exit 0'); } if (!shouldPass && result.status === 0) { throw new Error('failing video resource readiness audit should exit non-zero in strict mode'); } } function row({ briefId, role, creatorName, videoUrl, coverUrl, asrText, frameUrls, isReal }) { return [ briefId, '示例项目', '小红书', role, creatorName, `https://example.com/${creatorName}`, videoUrl, coverUrl, asrText, frameUrls, '示例标题', '2026-06-01', '内容摘要用于辅助判断视频风格。', '真实记录感|高质感女性场景', '真人出镜、自然光、轻口播', '', 'smoke fake source', isReal ]; } function writeCsv(file, rows) { fs.writeFileSync(file, `\uFEFF${rows.map(row => row.map(csvCell).join(',')).join('\n')}`, 'utf8'); } function csvCell(value) { const text = String(value ?? ''); return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; } function assert(condition, message) { if (!condition) throw new Error(message); } main();