180 lines
6.7 KiB
JavaScript
180 lines
6.7 KiB
JavaScript
|
|
/**
|
|||
|
|
* D136+ · 可灵视频生成 · 三层协议提示词 → Kling API
|
|||
|
|
*
|
|||
|
|
* 与 Seedance 版 generate-shots.js 共享同一套 buildPrompt() 逻辑。
|
|||
|
|
* 唯一区别: API 适配器不同(kling-api-adapter vs video-api-adapter)。
|
|||
|
|
*
|
|||
|
|
* 用法: node generate-kling-shots.js
|
|||
|
|
*
|
|||
|
|
* ⊢ 三层协议 (cc-022):
|
|||
|
|
* 上下文层: 英文编码 → 告诉AI前面发生了什么
|
|||
|
|
* 编码层: CHAR+PROP+ENV锁定 → 不可变信息
|
|||
|
|
* 场景层: 中文自然语言 → AI理解为什么后自己推理
|
|||
|
|
*
|
|||
|
|
* 铸渊 ICE-GL-ZY001 · D136+ · 2026-06-21
|
|||
|
|
*/
|
|||
|
|
const { generateVideo } = require('./kling-official-adapter');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const ENCODING_FILE = path.resolve(__dirname, '../outputs/付费修仙-ep01-director-encoding.json');
|
|||
|
|
const REGISTRY_FILE = path.resolve(__dirname, '../outputs/video-registry.json');
|
|||
|
|
|
|||
|
|
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'));
|
|||
|
|
|
|||
|
|
console.log(`[Kling Generate] ${encoding.project} · ep${encoding.episode} · ${encoding.shots.length}镜`);
|
|||
|
|
console.log(`[Kling Generate] 模型: kling-v2-6 · 约1.8元/镜`);
|
|||
|
|
console.log(`[Kling Generate] 输出: ${OUT_DIR}\n`);
|
|||
|
|
|
|||
|
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|||
|
|
|
|||
|
|
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 || '(无)'}`);
|
|||
|
|
|
|||
|
|
const shotName = `${encoding.project}-${encoding.episode}-${s.id}`;
|
|||
|
|
const outputPath = path.join(OUT_DIR, `kling-${shotName}.mp4`);
|
|||
|
|
|
|||
|
|
// 已存在则跳过
|
|||
|
|
if (fs.existsSync(outputPath)) {
|
|||
|
|
console.log(` ✅ Kling版已存在: ${path.basename(outputPath)},跳过\n`);
|
|||
|
|
results.push({ shot: s.id, file: outputPath, api: 'kling', status: 'cached' });
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(` [Prompt 预览]: ${prompt.substring(0, 120)}...`);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const result = await generateVideo({
|
|||
|
|
prompt,
|
|||
|
|
duration: s.duration || 5,
|
|||
|
|
outputPath,
|
|||
|
|
model: 'kling-v2-6',
|
|||
|
|
});
|
|||
|
|
console.log(` ✅ Kling生成完成: ${path.basename(result.videoPath)} (task: ${result.taskId})\n`);
|
|||
|
|
results.push({
|
|||
|
|
shot: s.id,
|
|||
|
|
file: result.videoPath,
|
|||
|
|
taskId: result.taskId,
|
|||
|
|
api: 'kling',
|
|||
|
|
model: 'kling-v2-6',
|
|||
|
|
status: 'completed',
|
|||
|
|
generatedAt: new Date().toISOString(),
|
|||
|
|
});
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error(` ❌ Kling生成失败: ${e.message}\n`);
|
|||
|
|
results.push({ shot: s.id, file: null, api: 'kling', status: 'failed', error: e.message });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 写入结果
|
|||
|
|
const resultFile = path.join(OUT_DIR, `kling-${encoding.project}-${encoding.episode}-results.json`);
|
|||
|
|
fs.writeFileSync(resultFile, JSON.stringify(results, null, 2));
|
|||
|
|
|
|||
|
|
// 更新video-registry
|
|||
|
|
updateRegistry(results, encoding);
|
|||
|
|
|
|||
|
|
const success = results.filter(r => r.status === 'completed').length;
|
|||
|
|
console.log(`\n═══ Kling完成: ${success}/${results.length} ═══`);
|
|||
|
|
console.log(`结果: ${resultFile}`);
|
|||
|
|
|
|||
|
|
if (success < results.length) {
|
|||
|
|
console.log(`⚠️ ${results.length - success}镜失败,详见结果文件。`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 三层协议 buildPrompt — 和 Seedance 版 generate-shots.js 完全一致的逻辑
|
|||
|
|
* ⊢ cc-022: 上下文层(英) + 编码层(中英) + 场景层(中)
|
|||
|
|
*/
|
|||
|
|
function buildPrompt(shot, encoding) {
|
|||
|
|
const locks = encoding.continuity_locks;
|
|||
|
|
let prompt = '';
|
|||
|
|
|
|||
|
|
// ─── [Context] 上下文层: 英文编码 → 告诉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\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`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── → 动作: 当前镜头的 spatial_anchor + 动作描述 ───
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新 video-registry.json 加入可灵生成的视频
|
|||
|
|
*/
|
|||
|
|
function updateRegistry(results, encoding) {
|
|||
|
|
let registry = { _meta: { updated: new Date().toISOString(), by: '铸渊 ICE-GL-ZY001' }, shots: {} };
|
|||
|
|
|
|||
|
|
if (fs.existsSync(REGISTRY_FILE)) {
|
|||
|
|
try { registry = JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf8')); }
|
|||
|
|
catch (e) { console.log(' ⚠️ video-registry.json 读取失败,新建。'); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const r of results) {
|
|||
|
|
if (r.status !== 'completed') continue;
|
|||
|
|
const shotId = `kling-${encoding.project}-${encoding.episode}-${r.shot}`;
|
|||
|
|
registry.shots[shotId] = {
|
|||
|
|
shotId,
|
|||
|
|
taskId: r.taskId || null,
|
|||
|
|
projectKey: `${encoding.project}/${encoding.episode}`,
|
|||
|
|
filePath: r.file,
|
|||
|
|
duration: 5,
|
|||
|
|
model: 'kling-v2-6',
|
|||
|
|
api: 'kling',
|
|||
|
|
generatedAt: r.generatedAt || new Date().toISOString(),
|
|||
|
|
dNumber: 'D136+',
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
registry._meta.updated = new Date().toISOString();
|
|||
|
|
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(registry, null, 2));
|
|||
|
|
console.log(`\n[Registry] video-registry.json 已更新 (Kling条目: ${Object.keys(registry.shots).filter(k => k.startsWith('kling-')).length})`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main().catch(e => { console.error(e); process.exit(1); });
|