aigc_report.mjs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. /**
  2. * aigc_report.mjs — AIGC 分镜拉片报告生成器(JSS-03)
  3. * 读取 aigc_remake.json + aigc_prompt.md → 产出 aigc_report.html(暗色/手机自适应/相对路径)
  4. * 纯 ESM, 零依赖。CLI: node aigc_report.mjs workdir
  5. */
  6. import fs from "node:fs";
  7. const esc = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
  8. const fmtTs = (ms) => { const s = Math.floor(ms / 1000); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; };
  9. function scoreBars(dims) {
  10. return dims.map((d) => `
  11. <div class="dim"><div class="dim-head"><span>${esc(d.name)}</span><span>${d.score}</span></div>
  12. <div class="bar"><i style="width:${d.score}%"></i></div><div class="note">${esc(d.note)}</div></div>`).join("");
  13. }
  14. function shotRows(board) {
  15. return board.map((s) => {
  16. const img = s.file ? `frames/${s.file}` : "";
  17. return `<details class="shot"><summary>
  18. <span class="idx">#${s.i + 1}</span><span class="t">${esc(s.t_label)}</span>
  19. <span class="dur">${s.dur}s</span><span class="role">${esc(s.role.split("(")[0])}</span></summary>
  20. <div class="shot-body">
  21. ${img ? `<img loading="lazy" src="${img}" alt="shot ${s.i}">` : ""}
  22. <p><b>画面:</b> ${esc(s.caption)}</p>
  23. ${s.lyric ? `<p><b>音频:</b> ${esc(s.lyric)}</p>` : ""}
  24. ${s.vision?.char ? `<p><b>主体:</b> ${esc(s.vision.char)}</p>` : ""}
  25. ${s.vision?.shot ? `<p><b>运镜:</b> ${esc(s.vision.shot)}</p>` : ""}
  26. ${s.vision?.style ? `<p><b>风格:</b> ${esc(s.vision.style)}</p>` : ""}
  27. ${s.vision?.aigc ? `<p class="aigc"><b>AI痕迹:</b> ${esc(s.vision.aigc)}</p>` : ""}
  28. </div></details>`;
  29. }).join("");
  30. }
  31. export function render(remake, promptMd) {
  32. const { video, type, structure, registry, storyboard, score, voc } = remake;
  33. const st = video.stats || {};
  34. return `<!DOCTYPE html><html lang="zh-CN" data-theme="dark"><head><meta charset="UTF-8">
  35. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  36. <title>AIGC分镜拉片:${esc((video.desc || "").split("#")[0].trim())}</title><style>
  37. :root{--bg:#0a0e17;--surface:#141a28;--line:#232c42;--tx:#dbe4f5;--dim:#8b98b8;--acc:#7c6cff;--gold:#ffb84d;--green:#3ddc97;--red:#ff6b7a}
  38. *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--tx);font:15px/1.7 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
  39. .wrap{max-width:860px;margin:0 auto;padding:16px}
  40. h1{font-size:20px}h2{font-size:17px;border-left:3px solid var(--acc);padding-left:10px;margin-top:32px}
  41. .card{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:14px;margin:10px 0}
  42. .stats{display:flex;flex-wrap:wrap;gap:8px}.stat{flex:1;min-width:90px;text-align:center;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:10px 4px}
  43. .stat b{display:block;font-size:18px;color:var(--gold)}.stat span{font-size:12px;color:var(--dim)}
  44. .badge{display:inline-block;background:var(--acc);color:#fff;border-radius:20px;padding:2px 12px;font-size:13px;margin-right:6px}
  45. video{width:100%;border-radius:12px;margin:10px 0;background:#000}
  46. details.shot{background:var(--surface);border:1px solid var(--line);border-radius:10px;margin:6px 0;overflow:hidden}
  47. details.shot summary{cursor:pointer;padding:9px 12px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;list-style:none}
  48. details.shot summary::-webkit-details-marker{display:none}
  49. .idx{color:var(--acc);font-weight:700;min-width:36px}.t{color:var(--dim);font-size:13px}.dur{color:var(--gold);font-size:13px}
  50. .role{font-size:12px;color:var(--dim)}
  51. .shot-body{padding:4px 14px 14px}.shot-body img{width:100%;border-radius:8px;margin:6px 0}
  52. .shot-body p{margin:4px 0;font-size:14px}.aigc{color:var(--red)}
  53. .bar{height:8px;background:var(--line);border-radius:4px;overflow:hidden}.bar i{display:block;height:100%;background:linear-gradient(90deg,var(--acc),var(--gold))}
  54. .dim{margin:10px 0}.dim-head{display:flex;justify-content:space-between;font-size:14px}.note{font-size:12px;color:var(--dim)}
  55. table{width:100%;border-collapse:collapse;font-size:13px}th,td{border:1px solid var(--line);padding:6px 8px;text-align:left}th{color:var(--gold);font-weight:600}
  56. pre{white-space:pre-wrap;word-break:break-word;background:#0d1220;border:1px solid var(--line);border-radius:10px;padding:14px;font-size:13px;max-height:420px;overflow:auto}
  57. a{color:var(--acc)}.sec{font-size:13px;color:var(--dim)}
  58. footer{text-align:center;color:var(--dim);font-size:12px;padding:24px 0}
  59. </style></head><body><div class="wrap">
  60. <h1>🕷️ AIGC 分镜拉片报告</h1>
  61. <div class="sec">${esc(video.author)} · <a href="${esc(video.url)}">原片</a> · ${fmtTs(video.duration_s * 1000)}</div>
  62. <div style="margin:10px 0">
  63. <span class="badge">${esc(type.primaryLabel || "AIGC内容")}</span>
  64. <span class="badge" style="background:var(--gold);color:#333">置信度 ${type.confidence ?? "?"}</span>
  65. <span class="badge" style="background:#2a3350">复刻模式: 分镜仿制</span>
  66. </div>
  67. <video controls preload="metadata" src="video.mp4"></video>
  68. <div class="stats">
  69. <div class="stat"><b>${(st.digg_count ?? 0).toLocaleString()}</b><span>👍 点赞</span></div>
  70. <div class="stat"><b>${(st.comment_count ?? 0).toLocaleString()}</b><span>💬 评论</span></div>
  71. <div class="stat"><b>${(st.collect_count ?? 0).toLocaleString()}</b><span>🔖 收藏</span></div>
  72. <div class="stat"><b>${(st.share_count ?? 0).toLocaleString()}</b><span>↗ 转发</span></div>
  73. <div class="stat"><b>${score.total}</b><span>综合评分</span></div>
  74. </div>
  75. <h2>🧬 复刻资产注册表</h2>
  76. <div class="card"><b>人物:</b> ${(registry.characters || []).map(esc).join("、") || "—"}<br>
  77. <b>场景:</b> ${(registry.scenes || []).map(esc).join("、") || "—"}<br>
  78. <b>风格:</b> ${(registry.styles || []).map(esc).join("、") || "—"}<br>
  79. <b>节奏:</b> ${structure.shot_count} 镜 / 平均 ${structure.avg_shot_s}s / ${(structure.shot_count / (video.duration_s / 60)).toFixed(0)} 镜每分钟</div>
  80. <h2>🎬 片体结构</h2>
  81. <table><tr><th>段落</th><th>时间</th><th>类型</th><th>内容</th></tr>
  82. ${(structure.sections || []).map((s) => `<tr><td>${esc(s.name)}</td><td class="t">${s.start}s–${s.end}s</td><td>${esc(s.type || "")}</td><td>${esc(s.content || s.lyric || "")}</td></tr>`).join("")}
  83. </table>
  84. <h2>🎞️ 分镜表(${storyboard.length} 镜)</h2>
  85. ${shotRows(storyboard)}
  86. <h2>📊 六维评分</h2>
  87. <div class="card">${scoreBars(score.dims)}</div>
  88. <h2>🧾 仿爆款长提示词</h2>
  89. <div class="sec">完整版: <a href="aigc_prompt.md" download>aigc_prompt.md</a> — 直接投喂给 AIGC 创作智能体, 替换占位符即可逐镜复刻</div>
  90. <pre>${esc(promptMd)}</pre>
  91. <h2>💬 评论区 VOC</h2>
  92. <div class="card">${(voc?.top || []).map((c) => `<div style="border-bottom:1px solid var(--line);padding:6px 0"><b style="color:var(--gold)">👍${c.likes}</b> ${esc(c.text)}</div>`).join("") || "—"}
  93. <div class="sec" style="margin-top:6px">共采集 ${voc?.total || 0} 条</div></div>
  94. <h2>🚀 改进与复刻策略</h2>
  95. <div class="card">${(remake.suggestions || []).map((s) => `<p>▸ ${esc(s)}</p>`).join("")}</div>
  96. <footer>skill-video-lapian · JSS-01/02/03 · ${new Date().toISOString().slice(0, 10)}</footer>
  97. </div></body></html>`;
  98. }
  99. if (import.meta.url === `file://${process.argv[1]}`) {
  100. const wd = process.argv[2] || ".";
  101. const remake = JSON.parse(fs.readFileSync(`${wd}/aigc_remake.json`, "utf8"));
  102. const promptMd = fs.existsSync(`${wd}/aigc_prompt.md`) ? fs.readFileSync(`${wd}/aigc_prompt.md`, "utf8") : "(未生成)";
  103. fs.writeFileSync(`${wd}/aigc_report.html`, render(remake, promptMd));
  104. console.log(`→ ${wd}/aigc_report.html (${(fs.statSync(`${wd}/aigc_report.html`).size / 1024).toFixed(0)} KB)`);
  105. }