guanghulab/video-ai-system/engines/generate-shots.js
冰朔 e5d2d5d4ef
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled
编码锁定边界精确化: CHAR+PROP+ENV全锁·情节+原因让AI推理
编码层(不可变): CHAR(人物)+PROP(道具)+ENV(环境·位置·背景)
  ⊢ CHAR-003 · 锁定: 18岁男性·白色长衫
  ⊢ PROP · 锁定: 竖式悬挂·【天道宗】
  ⊢ ENV-002 · 锁定: 修仙世界·百宗会广场·人群边缘角落

场景层(AI推理): 情节内容+为什么(动机·情感·因果)
  灵霄宗排队·天道宗无人问津·苏白有系统→不绝望
  喊话=宣言不是乞求·叹气=烦躁不是认命

冰朔: '编码锁人物+环境一致性,让AI自己推理情节+原因'
2026-06-21 17:32:50 +08:00

125 lines
4.5 KiB
JavaScript

/**
* 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;
// ═══ 编码+自然语言 双层协议 ═══
//
// ⊢ 编码层: 锁死不可变的
// CHAR(人物) + PROP(道具) + ENV(环境·空间·背景)
// 这些在任何镜头里都不能变。精简,只锁关键属性。
//
// ⊢ 场景层: 自然语言 → AI自己推理
// 情节内容 + 为什么(动机·情感·因果关系)
// 让AI理解原因→自己推理→生成有灵魂的画面
let prompt = '';
// ─── 编码层: CHAR+PROP+ENV全部锁定 ───
if (shot.char_ref && locks?.characters?.[shot.char_ref]) {
prompt += `⊢ CHAR-003 · 苏白 · 锁定\n`;
prompt += ` 18岁男性·白色长衫·黑色长发半束·175cm挺拔\n`;
}
if (shot.prop_ref && locks?.props?.[shot.prop_ref]) {
prompt += `⊢ PROP · 锁定\n`;
prompt += ` 竖式悬挂·破旧木质·【天道宗】\n`;
}
if (shot.prop_ref_2 && locks?.props?.[shot.prop_ref_2]) {
const propLock2 = locks.props[shot.prop_ref_2];
prompt += `⊢ PROP · 锁定\n`;
prompt += ` 横式立地·破旧木板·招生广告\n`;
}
// 环境锁定
if (shot.env && locks?.environments?.[shot.env]) {
prompt += `⊢ ENV-002 · 锁定\n`;
prompt += ` 修仙世界·百宗会广场·人群边缘角落·白天金色阳光\n`;
}
// ─── 场景层: 情节+原因 → AI推理 ───
if (encoding.scene) {
prompt += `\n⊢ 场景\n`;
prompt += ` ${encoding.scene}\n`;
}
// ─── 动作: 自然语言 → AI发挥 ───
prompt += `\n→ 景别: ${shot.framing}`;
if (shot.emotion?.type) prompt += ` | ${shot.emotion.type}`;
prompt += `\n`;
if (shot.action) {
prompt += `${shot.action}\n`;
}
// ─── 风格 ───
prompt += `⊢ 风格: 3D动画渲染·中国风仙侠·电影级光影\n`;
prompt += `⊢ 禁止: 真人写实·卡通·现代元素·水印`;
return prompt;
}
main().catch(e => { console.error(e); process.exit(1); });