/** * D136+ · 视频批量生成 · 导演编码 → Seedance API * 用法: node generate-shots.js */ const { generateVideo } = require('./video-api-adapter'); const fs = require('fs'); const path = require('path'); const ENCODING_FILE = path.resolve(__dirname, '../outputs/付费修仙-ep01-director-encoding.json'); // D136+ 输出优先JZAO外置硬盘 · 本地fallback const JZAO_SHOTS = '/Volumes/JZAO/铸渊-ICE-GL-ZY001/OUT-输出/视频/zai-fu-fei-xiu-xian/ep01'; const LOCAL_SHOTS = path.resolve(__dirname, '../outputs/shots'); const OUT_DIR = fs.existsSync(JZAO_SHOTS) ? JZAO_SHOTS : LOCAL_SHOTS; async function main() { const encoding = JSON.parse(fs.readFileSync(ENCODING_FILE, 'utf8')); fs.mkdirSync(OUT_DIR, { recursive: true }); console.log(`[Generate] ${encoding.project} · ep${encoding.episode} · ${encoding.shots.length}镜`); console.log(`[Generate] 输出: ${OUT_DIR}\n`); const results = []; for (let i = 0; i < encoding.shots.length; i++) { const s = encoding.shots[i]; const prompt = buildPrompt(s, encoding); console.log(`━━━ 镜${i + 1}/${encoding.shots.length}: ${s.id} ━━━`); console.log(` 景别: ${s.framing} | 情绪: ${s.emotion?.type}(${s.emotion?.intensity}) | ${s.duration}s`); console.log(` spatial_anchor: ${s.spatial_anchor}`); console.log(` text_elements: ${s.text_elements}`); console.log(` prompt: ${prompt.substring(0, 100)}...`); const outputPath = path.join(OUT_DIR, `${encoding.project}-${encoding.episode}-${s.id}.mp4`); if (fs.existsSync(outputPath)) { console.log(` ✅ 已存在,跳过\n`); results.push({ ...s, file: outputPath }); continue; } try { const result = await generateVideo({ prompt, duration: s.duration || 5, shotId: `${encoding.project}-${encoding.episode}-${s.id}`, projectKey: `付费修仙/ep01`, outputPath, }); console.log(` ✅ ${path.basename(result.videoPath)}\n`); results.push({ ...s, file: result.videoPath, taskId: result.taskId }); } catch (e) { console.error(` ❌ ${e.message}\n`); results.push({ ...s, file: null, error: e.message }); // 继续下一个 } } const resultFile = path.join(OUT_DIR, `${encoding.project}-${encoding.episode}-results.json`); fs.writeFileSync(resultFile, JSON.stringify(results, null, 2)); const success = results.filter(r => r.file).length; console.log(`\n═══ 完成: ${success}/${results.length} ═══`); console.log(`结果: ${resultFile}`); } function buildPrompt(shot, encoding) { const locks = encoding.continuity_locks; // ═══ 上下文+编码+自然语言 三层协议 ═══ // // [Context] 英文编码 · 告诉AI前面发生了什么 + 这个故事是什么 // 50词以内 → AI理解上下文 → 不自盲抽 // // ⊢ 编码层: CHAR+PROP+ENV锁定 // // ⊢ 场景层: 中文自然语言 → AI推理情节+原因 let prompt = ''; // ─── 上下文层: 英文编码 → 告诉AI前面发生了什么 ─── // 每镜都带:剧集摘要 + 前一个镜头的框架 if (encoding.episode_summary) { prompt += `[Ep${encoding.episode} Summary] ${encoding.episode_summary}\n`; } if (encoding.prev_shots?.[shot.id]) { prompt += `[Previous Shot] ${encoding.prev_shots[shot.id]}\n`; } if (prompt.length > 0) prompt += '\n'; // ─── 编码层: CHAR+PROP+ENV ─── if (shot.char_ref && locks?.characters?.[shot.char_ref]) { prompt += `⊢ CHAR-003 Su Bai: 18yr male, white robe, black hair half-up, 175cm\n`; } if (shot.prop_ref && locks?.props?.[shot.prop_ref]) { prompt += `⊢ PROP: vertical hanging wood sign, worn edges, 【天道宗】\n`; } if (shot.prop_ref_2 && locks?.props?.[shot.prop_ref_2]) { prompt += `⊢ PROP: horizontal floor board, recruitment ad\n`; } if (shot.env && locks?.environments?.[shot.env]) { prompt += `⊢ ENV: cultivation square, edge corner, golden sunlight\n`; } // ─── 场景层: 情节+原因 → AI推理 ─── if (encoding.scene) { prompt += `\n⊢ Scene:\n ${encoding.scene}\n`; } // ─── 动作: 当前镜头 ─── prompt += `\n→ Shot: ${shot.framing}`; if (shot.emotion?.type) prompt += ` | ${shot.emotion.type}`; prompt += `\n`; if (shot.action) { prompt += `→ ${shot.action}\n`; } prompt += `⊢ Style: 3D animation, Chinese cultivation, cinematic lighting\n`; prompt += `⊢ No: photorealism, cartoon, modern elements, watermark`; return prompt; } main().catch(e => { console.error(e); process.exit(1); });