guanghulab/video-ai-system/engines/generate-shots.js

130 lines
4.9 KiB
JavaScript
Raw Normal View History

/**
* 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;
// ═══ 编码+自然语言 双层协议 ═══
// 冰朔: 编码锁死不可变的(人物/道具)→ 只锁关键属性,精简。
// 自然语言解释为什么(场景/动机)→ 让AI理解原因→自己推理。
// 如果全锁死AI没有了"灵光一闪"的可能性。
//
// 三层结构:
// ⊢ 编码层 (锁死·不可变) ← 精简CHAR/PROP关键属性
// ⊢ 场景层 (自然语言·理解) ← AI理解为什么→自己推理
// → 动作层 (自然语言·发挥) ← 具体做什么·给AI空间
let prompt = '';
// ─── 编码层: 只锁关键属性 ───
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]) {
const propLock = locks.props[shot.prop_ref];
prompt += `${shot.prop_ref} · 编码锁定\n`;
prompt += ` 竖式悬挂·破旧木质·【天道宗】\n`;
}
if (shot.prop_ref_2 && locks?.props?.[shot.prop_ref_2]) {
const propLock2 = locks.props[shot.prop_ref_2];
prompt += `${shot.prop_ref_2} · 编码锁定\n`;
prompt += ` 横式立地·破旧木板·招生广告: ${propLock2.text}\n`;
}
// ─── 场景层: 自然语言 → AI理解为什么 ───
if (encoding.scene) {
prompt += `\n⊢ 场景 · 自然语言理解\n`;
prompt += ` ${encoding.scene}\n`;
}
// ─── 空间 ───
if (shot.env && locks?.environments?.[shot.env]) {
const hasChar = !!(shot.char_ref && locks?.characters?.[shot.char_ref]);
if (!hasChar) {
// 纯环境镜
prompt += `\n⊢ 空间\n`;
prompt += ` 修仙广场·人群边缘角落·金色阳光·3D动画渲染\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); });