2026-06-21 16:38:20 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 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');
|
2026-06-21 16:56:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 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;
|
2026-06-21 16:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
const parts = [];
|
|
|
|
|
|
|
2026-06-21 17:07:19 +08:00
|
|
|
|
// === D136+ 分层提示词引擎 ===
|
|
|
|
|
|
// 冰朔: "用编码告诉他这是什么意思" → HLDP编号不是标签,是优先级排序
|
|
|
|
|
|
//
|
|
|
|
|
|
// 提示词分层:
|
|
|
|
|
|
// L0 主体(人物+道具) ← AI注意力第一落点·不可稀释
|
|
|
|
|
|
// L1 空间(克制描述) ← 只给必要信息·不抢人物焦点
|
|
|
|
|
|
// L2 动作 ← 具体行为
|
|
|
|
|
|
// L3 风格/约束 ← 全局修饰
|
2026-06-21 16:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
let desc = '';
|
|
|
|
|
|
|
2026-06-21 17:07:19 +08:00
|
|
|
|
// ─── L0: 主体锁定 ───
|
|
|
|
|
|
// 人物和道具的描述放在最前面,确保AI第一注意力在这里
|
|
|
|
|
|
const l0Parts = [];
|
2026-06-21 16:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
if (shot.char_ref && locks?.characters?.[shot.char_ref]) {
|
|
|
|
|
|
const charLock = locks.characters[shot.char_ref];
|
2026-06-21 17:07:19 +08:00
|
|
|
|
l0Parts.push(charLock.locked_desc);
|
2026-06-21 16:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (shot.prop_ref && locks?.props?.[shot.prop_ref]) {
|
|
|
|
|
|
const propLock = locks.props[shot.prop_ref];
|
2026-06-21 17:07:19 +08:00
|
|
|
|
l0Parts.push(propLock.locked_desc);
|
2026-06-21 16:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (shot.prop_ref_2 && locks?.props?.[shot.prop_ref_2]) {
|
|
|
|
|
|
const propLock2 = locks.props[shot.prop_ref_2];
|
2026-06-21 17:07:19 +08:00
|
|
|
|
l0Parts.push(propLock2.locked_desc);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没人也没道具(纯环境镜)→不生成L0
|
|
|
|
|
|
if (l0Parts.length > 0) {
|
|
|
|
|
|
desc += l0Parts.join('。');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── L1: 空间定位 ───
|
|
|
|
|
|
// 克制描述。不给灵霄宗鎏金大字——除非是S1-01建立镜
|
|
|
|
|
|
if (shot.env && locks?.environments?.[shot.env]) {
|
|
|
|
|
|
const envLock = locks.environments[shot.env];
|
|
|
|
|
|
// 有主体时用简短版·没有主体时用完整版
|
|
|
|
|
|
if (l0Parts.length > 0) {
|
|
|
|
|
|
// 简短版: 去掉"灵霄宗鎏金大字"细节,只留位置和光线
|
|
|
|
|
|
desc += `。背景: 修仙广场边缘角落,人群远处,金色阳光`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
desc += envLock.locked_desc;
|
|
|
|
|
|
}
|
2026-06-21 16:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 17:07:19 +08:00
|
|
|
|
// ─── L2: 动作 ───
|
2026-06-21 16:38:20 +08:00
|
|
|
|
if (shot.action) {
|
|
|
|
|
|
desc += `。${shot.action}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关键信息追加
|
|
|
|
|
|
if (shot.text_elements) desc += `。画面中可见文字: ${shot.text_elements}`;
|
|
|
|
|
|
if (shot.prop_state) desc += `。${shot.prop_state}`;
|
|
|
|
|
|
|
|
|
|
|
|
parts.push(desc);
|
|
|
|
|
|
|
2026-06-21 17:07:19 +08:00
|
|
|
|
// ─── L3: 风格 ───
|
|
|
|
|
|
parts.push('景别: ' + shot.framing);
|
2026-06-21 16:38:20 +08:00
|
|
|
|
|
2026-06-21 17:07:19 +08:00
|
|
|
|
if (shot.emotion?.type) parts.push('氛围: ' + shot.emotion.type);
|
2026-06-21 16:38:20 +08:00
|
|
|
|
|
2026-06-21 17:07:19 +08:00
|
|
|
|
// 风格锁定(不再是第一行)
|
|
|
|
|
|
parts.push('真人写实风格,电影级光影,亚洲面孔');
|
|
|
|
|
|
parts.push('不要卡通·不要3D渲染·不要现代元素·不要水印·不要字幕');
|
2026-06-21 16:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
return parts.join('。');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main().catch(e => { console.error(e); process.exit(1); });
|