// 只跑【文字 + 音频】两模块(不调 Remotion、不需要 Chrome),把 VideoDocument 契约 // 导出成 JSON,并打印一份人读摘要。用于快速验证重构后的契约与口播/卡片/字幕。 // // 用法: node scripts/dump-video-document.mjs // TEMPLATE=knowledge INPUT=test/fixtures/sample-knowledge.json node scripts/dump-video-document.mjs // NO_TTS=1 node scripts/dump-video-document.mjs # 跳过真实 TTS(用静音估算时长) // // 需要的:.env 里的 LLM key(除非用 INPUT 直接给合法 JSON + skipLlm)。TTS key(除非 NO_TTS)。 import { readFile, writeFile, mkdir } from "node:fs/promises"; import { existsSync } from "node:fs"; import { fileURLToPath, pathToFileURL } from "node:url"; import { createRequire } from "node:module"; import { resolve, join } from "node:path"; const root = resolve(fileURLToPath(new URL("..", import.meta.url))); process.chdir(root); // --- .env (填充未设置的 key) --- const envPath = join(root, ".env"); if (existsSync(envPath)) { for (const line of (await readFile(envPath, "utf8")).split("\n")) { const t = line.trim(); if (!t || t.startsWith("#")) continue; const eq = t.indexOf("="); if (eq < 0) continue; const k = t.slice(0, eq).trim(); if (k && process.env[k] === undefined) process.env[k] = t.slice(eq + 1).trim(); } } // --- config/default.yaml (yaml 通过 createRequire 从 shared 的依赖上下文解析出确切 dist 路径) --- const sharedRequire = createRequire(join(root, "packages/shared/package.json")); const yamlPath = sharedRequire.resolve("yaml"); const yamlMod = await import(pathToFileURL(yamlPath).href); const config = yamlMod.parse(await readFile(join(root, "config/default.yaml"), "utf8")); // --- 模块(从编译好的 dist 导入) --- const { generateDocument } = await import(join(root, "packages/text/dist/index.js")); const { generateAudio } = await import(join(root, "packages/audio/dist/index.js")); const { resolveOutputDir } = await import(join(root, "packages/shared/dist/node.js")); const template = process.env.TEMPLATE ?? "github-trending"; const source = process.env.INPUT ? undefined : (process.env.SOURCE ?? "github-trending"); const providerName = process.env.TTS_PROVIDER ?? config.tts?.provider ?? "openai-tts"; const providerConfig = config.tts?.[providerName]; const outputDir = resolveOutputDir(config.output?.dir); const workDir = join(outputDir, "tmp", "dump"); await mkdir(workDir, { recursive: true }); const text = process.env.INPUT ? await readFile(resolve(process.env.INPUT), "utf8") : undefined; console.log(`\n=== 文字模块 (template=${template}, source=${source ?? "(input file)"}) ===`); const { doc: textDoc, publish } = await generateDocument({ template, text, source, sourceArgs: undefined, collectorConfig: source ? config.collect?.[source] : undefined, llm: { model: process.env.LLM_MODEL ?? config.llm?.model ?? "glm-5.1" }, skipLlm: process.env.SKIP_LLM === "1", }); let doc = textDoc; await writeFile(join(workDir, "doc-after-text.json"), JSON.stringify(doc, null, 2)); console.log(` ✓ 产出了 ${doc.data.length} 个段(cover=${doc.data.find(s => s.kind === "cover") ? "有" : "无"})。已写到 ${join(workDir, "doc-after-text.json")}`); if (publish) console.log(` publish (独立产物,不在 VideoDocument 内): title="${publish.title}" tags=[${(publish.tags ?? []).join(",")}]`); console.log(`\n=== 音频模块 (provider=${providerName}${process.env.NO_TTS === "1" ? ", 静音" : ""}) ===`); doc = await generateAudio(doc, { workDir, provider: providerName, voiceId: providerConfig?.defaultVoice, model: providerConfig?.model, format: config.tts?.format, speed: config.tts?.speed, skip: process.env.NO_TTS === "1", alignment: config.tts?.alignment, }); await writeFile(join(workDir, "doc-after-audio.json"), JSON.stringify(doc, null, 2)); // --- 人读摘要 --- const totalDur = doc.data.reduce((a, s) => a + (s.duration ?? 0), 0); console.log(`\n================ VideoDocument 摘要 ================`); console.log(`template=${doc.template} 段数=${doc.data.length} 总时长≈${totalDur.toFixed(1)}s`); console.log(`meta: title="${doc.meta?.title ?? ""}" subtitle="${doc.meta?.subtitle ?? ""}" trendSummary="${doc.meta?.trendSummary ?? ""}"`); if (publish) console.log(`publish (独立): title="${publish.title}" tags=[${(publish.tags ?? []).join(",")}]`); console.log(`----------------------------------------------------`); for (const s of doc.data) { const cards = s.cardList?.length ?? 0; const caps = s.caption?.length ?? 0; const origin = (s.captionOrigin ?? "").replace(/\s+/g, " ").slice(0, 48); console.log(` [${s.id}] kind=${s.kind} dur=${(s.duration ?? 0).toFixed(1)}s cards=${cards} captions=${caps}`); if (origin) console.log(` 口播: ${origin}${(s.captionOrigin ?? "").length > 48 ? "…" : ""}`); if (s.extension?.type === "github-trending") { const g = s.extension; console.log(` repo: ${g.repo.fullName} ★${g.repo.stars ?? "?"} 今日+${g.repo.todayStars ?? "?"} (extension)`); } } console.log(`====================================================`); console.log(`完整 JSON: ${join(workDir, "doc-after-audio.json")}`); console.log(`\n下一步: 确认口播/卡片/字幕无误后,用 ./scripts/verify-refactor.sh 跑完整渲染。`);