dump-video-document.mjs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. // 只跑【文字 + 音频】两模块(不调 Remotion、不需要 Chrome),把 VideoDocument 契约
  2. // 导出成 JSON,并打印一份人读摘要。用于快速验证重构后的契约与口播/卡片/字幕。
  3. //
  4. // 用法: node scripts/dump-video-document.mjs
  5. // TEMPLATE=knowledge INPUT=test/fixtures/sample-knowledge.json node scripts/dump-video-document.mjs
  6. // NO_TTS=1 node scripts/dump-video-document.mjs # 跳过真实 TTS(用静音估算时长)
  7. //
  8. // 需要的:.env 里的 LLM key(除非用 INPUT 直接给合法 JSON + skipLlm)。TTS key(除非 NO_TTS)。
  9. import { readFile, writeFile, mkdir } from "node:fs/promises";
  10. import { existsSync } from "node:fs";
  11. import { fileURLToPath, pathToFileURL } from "node:url";
  12. import { createRequire } from "node:module";
  13. import { resolve, join } from "node:path";
  14. const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
  15. process.chdir(root);
  16. // --- .env (填充未设置的 key) ---
  17. const envPath = join(root, ".env");
  18. if (existsSync(envPath)) {
  19. for (const line of (await readFile(envPath, "utf8")).split("\n")) {
  20. const t = line.trim();
  21. if (!t || t.startsWith("#")) continue;
  22. const eq = t.indexOf("=");
  23. if (eq < 0) continue;
  24. const k = t.slice(0, eq).trim();
  25. if (k && process.env[k] === undefined) process.env[k] = t.slice(eq + 1).trim();
  26. }
  27. }
  28. // --- config/default.yaml (yaml 通过 createRequire 从 shared 的依赖上下文解析出确切 dist 路径) ---
  29. const sharedRequire = createRequire(join(root, "packages/shared/package.json"));
  30. const yamlPath = sharedRequire.resolve("yaml");
  31. const yamlMod = await import(pathToFileURL(yamlPath).href);
  32. const config = yamlMod.parse(await readFile(join(root, "config/default.yaml"), "utf8"));
  33. // --- 模块(从编译好的 dist 导入) ---
  34. const { generateDocument } = await import(join(root, "packages/text/dist/index.js"));
  35. const { generateAudio } = await import(join(root, "packages/audio/dist/index.js"));
  36. const { resolveOutputDir } = await import(join(root, "packages/shared/dist/node.js"));
  37. const template = process.env.TEMPLATE ?? "github-trending";
  38. const source = process.env.INPUT ? undefined : (process.env.SOURCE ?? "github-trending");
  39. const providerName = process.env.TTS_PROVIDER ?? config.tts?.provider ?? "openai-tts";
  40. const providerConfig = config.tts?.[providerName];
  41. const outputDir = resolveOutputDir(config.output?.dir);
  42. const workDir = join(outputDir, "tmp", "dump");
  43. await mkdir(workDir, { recursive: true });
  44. const text = process.env.INPUT
  45. ? await readFile(resolve(process.env.INPUT), "utf8")
  46. : undefined;
  47. console.log(`\n=== 文字模块 (template=${template}, source=${source ?? "(input file)"}) ===`);
  48. const { doc: textDoc, publish } = await generateDocument({
  49. template,
  50. text,
  51. source,
  52. sourceArgs: undefined,
  53. collectorConfig: source ? config.collect?.[source] : undefined,
  54. llm: { model: process.env.LLM_MODEL ?? config.llm?.model ?? "glm-5.1" },
  55. skipLlm: process.env.SKIP_LLM === "1",
  56. });
  57. let doc = textDoc;
  58. await writeFile(join(workDir, "doc-after-text.json"), JSON.stringify(doc, null, 2));
  59. console.log(` ✓ 产出了 ${doc.data.length} 个段(cover=${doc.data.find(s => s.kind === "cover") ? "有" : "无"})。已写到 ${join(workDir, "doc-after-text.json")}`);
  60. if (publish) console.log(` publish (独立产物,不在 VideoDocument 内): title="${publish.title}" tags=[${(publish.tags ?? []).join(",")}]`);
  61. console.log(`\n=== 音频模块 (provider=${providerName}${process.env.NO_TTS === "1" ? ", 静音" : ""}) ===`);
  62. doc = await generateAudio(doc, {
  63. workDir,
  64. provider: providerName,
  65. voiceId: providerConfig?.defaultVoice,
  66. model: providerConfig?.model,
  67. format: config.tts?.format,
  68. speed: config.tts?.speed,
  69. skip: process.env.NO_TTS === "1",
  70. alignment: config.tts?.alignment,
  71. });
  72. await writeFile(join(workDir, "doc-after-audio.json"), JSON.stringify(doc, null, 2));
  73. // --- 人读摘要 ---
  74. const totalDur = doc.data.reduce((a, s) => a + (s.duration ?? 0), 0);
  75. console.log(`\n================ VideoDocument 摘要 ================`);
  76. console.log(`template=${doc.template} 段数=${doc.data.length} 总时长≈${totalDur.toFixed(1)}s`);
  77. console.log(`meta: title="${doc.meta?.title ?? ""}" subtitle="${doc.meta?.subtitle ?? ""}" trendSummary="${doc.meta?.trendSummary ?? ""}"`);
  78. if (publish) console.log(`publish (独立): title="${publish.title}" tags=[${(publish.tags ?? []).join(",")}]`);
  79. console.log(`----------------------------------------------------`);
  80. for (const s of doc.data) {
  81. const cards = s.cardList?.length ?? 0;
  82. const caps = s.caption?.length ?? 0;
  83. const origin = (s.captionOrigin ?? "").replace(/\s+/g, " ").slice(0, 48);
  84. console.log(` [${s.id}] kind=${s.kind} dur=${(s.duration ?? 0).toFixed(1)}s cards=${cards} captions=${caps}`);
  85. if (origin) console.log(` 口播: ${origin}${(s.captionOrigin ?? "").length > 48 ? "…" : ""}`);
  86. if (s.extension?.type === "github-trending") {
  87. const g = s.extension;
  88. console.log(` repo: ${g.repo.fullName} ★${g.repo.stars ?? "?"} 今日+${g.repo.todayStars ?? "?"} (extension)`);
  89. }
  90. }
  91. console.log(`====================================================`);
  92. console.log(`完整 JSON: ${join(workDir, "doc-after-audio.json")}`);
  93. console.log(`\n下一步: 确认口播/卡片/字幕无误后,用 ./scripts/verify-refactor.sh 跑完整渲染。`);