video-resource-readiness-smoke.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const { spawnSync } = require('child_process');
  6. const { VIDEO_RESOURCE_HEADER } = require('./create-video-intake-pack');
  7. const root = path.resolve(__dirname, '..');
  8. function main() {
  9. const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-video-resource-readiness-'));
  10. const passCsv = path.join(tmp, 'video-pass.csv');
  11. const failCsv = path.join(tmp, 'video-fail.csv');
  12. const passOutput = path.join(tmp, 'pass-output');
  13. const failOutput = path.join(tmp, 'fail-output');
  14. writeCsv(passCsv, [
  15. VIDEO_RESOURCE_HEADER,
  16. row({
  17. briefId: 'brief-001',
  18. role: '参考视频',
  19. creatorName: '参考博主',
  20. videoUrl: 'https://example.com/reference.mp4',
  21. coverUrl: 'https://example.com/reference-cover.jpg',
  22. asrText: '真实 ASR 文本,描述人群、场景、口播结构和表达方式。',
  23. frameUrls: 'https://example.com/reference-frame.jpg',
  24. isReal: '是'
  25. }),
  26. row({
  27. briefId: 'brief-001',
  28. role: '候选视频',
  29. creatorName: '候选博主',
  30. videoUrl: 'https://example.com/candidate.mp4',
  31. coverUrl: 'https://example.com/candidate-cover.jpg',
  32. asrText: '候选视频 ASR 文本,能够辅助判断调性是否接近。',
  33. frameUrls: '',
  34. isReal: '是'
  35. })
  36. ]);
  37. writeCsv(failCsv, [
  38. VIDEO_RESOURCE_HEADER,
  39. row({
  40. briefId: 'brief-001',
  41. role: '参考视频',
  42. creatorName: '参考博主',
  43. videoUrl: '',
  44. coverUrl: '',
  45. asrText: '',
  46. frameUrls: '',
  47. isReal: '否'
  48. })
  49. ]);
  50. runAudit(passCsv, passOutput, true);
  51. runAudit(failCsv, failOutput, false);
  52. const passSummary = JSON.parse(fs.readFileSync(path.join(passOutput, 'video-resource-readiness-summary.json'), 'utf8'));
  53. const failSummary = JSON.parse(fs.readFileSync(path.join(failOutput, 'video-resource-readiness-summary.json'), 'utf8'));
  54. const report = fs.readFileSync(path.join(passOutput, 'video-resource-readiness-report.md'), 'utf8').replace(/^\uFEFF/, '');
  55. const failReport = fs.readFileSync(path.join(failOutput, 'video-resource-readiness-report.md'), 'utf8').replace(/^\uFEFF/, '');
  56. const failRepairCsv = path.join(failOutput, 'video-resource-repair-actions.csv');
  57. assert(passSummary.acceptance.readyForVideoAbPreflight === true, 'complete video resources should pass preflight readiness');
  58. assert(Array.isArray(passSummary.repairActions) && passSummary.repairActions.length === 0, 'passing sample should have no repair actions');
  59. assert(passSummary.counts.realReferenceRows === 1, 'passing sample should include one real reference row');
  60. assert(passSummary.counts.realCandidateRows === 1, 'passing sample should include one real candidate row');
  61. assert(passSummary.counts.videoUrlRows === 2, 'passing sample should include video URLs');
  62. assert(failSummary.acceptance.readyForVideoAbPreflight === false, 'missing video resources should fail readiness');
  63. assert(Array.isArray(failSummary.repairActions) && failSummary.repairActions.length === failSummary.failureCount, 'failing sample should expose repair action for each issue');
  64. assert(fs.existsSync(failRepairCsv), 'failing audit should write repair actions CSV');
  65. assert(failSummary.issues.some(item => item.type === 'missing-real-reference'), 'failure should mention missing real reference resource');
  66. assert(failSummary.issues.some(item => item.type === 'missing-video-url'), 'failure should mention missing video URL');
  67. assert(report.includes('# 视频资源就绪审计'), 'report should be Chinese');
  68. assert(failReport.includes('## 修复清单'), 'failing report should include repair action section');
  69. assert(fs.readFileSync(failRepairCsv, 'utf8').includes('修复动作'), 'video repair action CSV should include action column');
  70. 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');
  71. console.log(JSON.stringify({
  72. ok: true,
  73. passOutput,
  74. failOutput,
  75. passFailureCount: passSummary.failureCount,
  76. failFailureCount: failSummary.failureCount
  77. }, null, 2));
  78. }
  79. function runAudit(input, output, shouldPass) {
  80. const result = spawnSync(process.execPath, [
  81. path.join(root, 'scripts', 'video-resource-readiness-audit.js'),
  82. '--input',
  83. input,
  84. '--output',
  85. output,
  86. '--strict'
  87. ], { cwd: root, encoding: 'utf8' });
  88. if (shouldPass && result.status !== 0) {
  89. process.stderr.write(result.stderr || result.stdout || '');
  90. throw new Error('passing video resource readiness audit should exit 0');
  91. }
  92. if (!shouldPass && result.status === 0) {
  93. throw new Error('failing video resource readiness audit should exit non-zero in strict mode');
  94. }
  95. }
  96. function row({ briefId, role, creatorName, videoUrl, coverUrl, asrText, frameUrls, isReal }) {
  97. return [
  98. briefId,
  99. '示例项目',
  100. '小红书',
  101. role,
  102. creatorName,
  103. `https://example.com/${creatorName}`,
  104. videoUrl,
  105. coverUrl,
  106. asrText,
  107. frameUrls,
  108. '示例标题',
  109. '2026-06-01',
  110. '内容摘要用于辅助判断视频风格。',
  111. '真实记录感|高质感女性场景',
  112. '真人出镜、自然光、轻口播',
  113. '',
  114. 'smoke fake source',
  115. isReal
  116. ];
  117. }
  118. function writeCsv(file, rows) {
  119. fs.writeFileSync(file, `\uFEFF${rows.map(row => row.map(csvCell).join(',')).join('\n')}`, 'utf8');
  120. }
  121. function csvCell(value) {
  122. const text = String(value ?? '');
  123. return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
  124. }
  125. function assert(condition, message) {
  126. if (!condition) throw new Error(message);
  127. }
  128. main();