generate-shots.js: 风格约束从真人→3D GLOBAL-NAV: 记录风格切换决策 STATUS.hdlp: 同步风格变更 导航: 3D验证图HLDP编码通过 根源: 冰朔决策在先但未写入导航→铸渊生成真人图
150 lines
6.0 KiB
JavaScript
150 lines
6.0 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;
|
||
|
||
// ═══ D136+ HLDP编码提示词引擎 ═══
|
||
// 冰朔: "编码的结构肯定有更高的权重。AI拿到编码后去组合编码,
|
||
// 然后照着编码去找你后面给他翻译的汉字。"
|
||
//
|
||
// 原理: HLDP的 ⊢ / · / | / → 这些标记符号天然构成层级结构。
|
||
// AI模型的注意力机制对结构化输入敏感——
|
||
// 每个 ⊢ 是一个锚点,每个 · 是一个属性分支。
|
||
// 编码权重 > 自然语言翻译权重。
|
||
//
|
||
// 结构:
|
||
// ⊢ CHAR · 锁定 ← 最高权重·不可变
|
||
// ⊢ PROP · 锁定 ← 紧随其后
|
||
// ⊢ 空间 ← 克制描述
|
||
// → 动作(自然语言翻译) ← 编码后的汉字辅助
|
||
|
||
let prompt = '';
|
||
|
||
// ─── 编码区: CHAR锁定 ───
|
||
if (shot.char_ref && locks?.characters?.[shot.char_ref]) {
|
||
const charLock = locks.characters[shot.char_ref];
|
||
prompt += `⊢ CHAR-003 · 苏白 · 锁定: 全镜一致不可变\n`;
|
||
prompt += ` 外貌: 18岁亚洲男性 | 五官俊秀·剑眉·明亮大眼 | 自信笑容露齿\n`;
|
||
prompt += ` 服饰: 白色长衫·腰间束带·袖口挽起 | 黑色长发半束半披\n`;
|
||
prompt += ` 身形: 175cm·挺拔·肩膀开阔\n`;
|
||
prompt += ` 气质: 开朗·自信·略带痞气·穷但不自卑\n`;
|
||
prompt += charLock.locked_desc + '\n';
|
||
}
|
||
|
||
// ─── 编码区: PROP锁定 ───
|
||
if (shot.prop_ref && locks?.props?.[shot.prop_ref]) {
|
||
const propLock = locks.props[shot.prop_ref];
|
||
prompt += `⊢ PROP · ${shot.prop_ref} · 锁定: 不可变\n`;
|
||
prompt += ` 方向: ${propLock.orientation} | 材质: ${propLock.material}\n`;
|
||
prompt += ` 文字: ${propLock.text}\n`;
|
||
prompt += propLock.locked_desc + '\n';
|
||
}
|
||
if (shot.prop_ref_2 && locks?.props?.[shot.prop_ref_2]) {
|
||
const propLock2 = locks.props[shot.prop_ref_2];
|
||
prompt += `⊢ PROP · ${shot.prop_ref_2} · 锁定: 不可变\n`;
|
||
prompt += ` 方向: ${propLock2.orientation} | 材质: ${propLock2.material}\n`;
|
||
prompt += ` 文字: ${propLock2.text}\n`;
|
||
prompt += propLock2.locked_desc + '\n';
|
||
}
|
||
|
||
// ─── 编码区: 空间 ───
|
||
if (shot.env && locks?.environments?.[shot.env]) {
|
||
const hasChar = !!(shot.char_ref && locks?.characters?.[shot.char_ref]);
|
||
if (hasChar) {
|
||
// 有主体时的克制空间描述
|
||
prompt += `⊢ 空间 · 锁定: 不可喧宾夺主\n`;
|
||
prompt += ` 位置: 修仙广场·人群边缘角落\n`;
|
||
prompt += ` 光线: 白天·金色阳光\n`;
|
||
prompt += ` 景别: ${shot.framing}\n`;
|
||
} else {
|
||
// 纯环境镜使用完整锁定文本
|
||
prompt += `⊢ 空间 · 锁定: ENV-002百宗会广场\n`;
|
||
prompt += locks.environments[shot.env].locked_desc + '\n';
|
||
}
|
||
}
|
||
|
||
// ─── 翻译区: 动作描述 ───
|
||
if (shot.action) {
|
||
prompt += `→ ${shot.action}\n`;
|
||
}
|
||
|
||
// 文字标注
|
||
if (shot.text_elements) {
|
||
prompt += `→ 画面中可见文字: ${shot.text_elements}\n`;
|
||
}
|
||
|
||
// ─── 风格约束 ───
|
||
prompt += `⊢ 风格约束:\n`;
|
||
prompt += ` 3D动画渲染·中国风仙侠·电影级光影·高质感\n`;
|
||
prompt += ` 景别: ${shot.framing}`;
|
||
if (shot.emotion?.type) prompt += ` | 氛围: ${shot.emotion.type}`;
|
||
prompt += `\n`;
|
||
prompt += ` 禁止: 真人写实·卡通·现代元素·水印·字幕`;
|
||
|
||
return prompt;
|
||
}
|
||
|
||
main().catch(e => { console.error(e); process.exit(1); });
|