cang-ying/engines/hldp-director-adapter.js

131 lines
4.2 KiB
JavaScript
Raw Normal View History

/**
* 光湖视频AI系统 · 导演编码适配器
* D136+ · 铸渊 ICE-GL-ZY001
*
* 输入Agent_06 导演编码JSON
* 输出video-editor.js edit() 参数
*
* 使用
* const { adapt } = require('./hldp-director-adapter');
* const editorInput = adapt(directorEncoding, { shotsDir: './outputs/shots/' });
* await edit(editorInput);
*/
// ==================== 景别 → zoom ====================
const FRAMING_ZOOM = {
'远景': { from: 1.0, to: 1.0 },
'全景': { from: 1.0, to: 1.05 },
'中景': { from: 1.0, to: 1.10 },
'近景': { from: 1.0, to: 1.15 },
'特写': { from: 1.0, to: 1.25 },
'大特写': { from: 1.0, to: 1.5 },
};
// ==================== 情绪类型 → 调色 ====================
const EMOTION_COLOR = {
'悬': { contrast: 1.2, saturation: 0.9, brightness: -0.05 },
'恐': { contrast: 1.2, saturation: 0.9, brightness: -0.05 },
'怒': { contrast: 1.15, saturation: 1.1, brightness: 0 },
'燃': { contrast: 1.15, saturation: 1.1, brightness: 0 },
'甜': { contrast: 1.0, saturation: 1.15, brightness: 0.05 },
'暖': { contrast: 1.0, saturation: 1.15, brightness: 0.05 },
'冷': { contrast: 0.95, saturation: 0.85, brightness: -0.08 },
'哀': { contrast: 0.95, saturation: 0.85, brightness: -0.08 },
'爽': { contrast: 1.1, saturation: 1.1, brightness: 0.02 },
'震': { contrast: 1.1, saturation: 1.1, brightness: 0.02 },
};
// ==================== 强度 → trim时长 ====================
function intensityToTrim(intensity) {
if (intensity <= 3) return 2.0;
if (intensity <= 6) return 3.0;
if (intensity <= 8) return 3.5;
return 4.5; // 9-10: 定格感
}
// ==================== 节奏 → 过渡类型 ====================
function rhythmToTransition(rhythm) {
switch (rhythm) {
case '铺垫': case '建立': case '余波': case '余韵':
return 'fade';
case '爬升': case '高潮': case '反转': case '转折':
return 'cut';
default: return 'fade';
}
}
// ==================== 主适配函数 ====================
/**
* @param {object} dc - 导演编码 (Agent_06 输出)
* @param {object} opts
* @param {string} [opts.shotsDir] - 分镜视频文件目录
* @param {string} [opts.voiceFile] - 配音文件
* @param {string} [opts.bgmFile] - BGM文件
* @param {string} [opts.srtFile] - 字幕文件
* @param {object} [opts.resolution] - { w, h }
* @param {number} [opts.fps]
* @param {string} [opts.output] - 输出路径
* @returns {object} video-editor.js edit() 参数
*/
function adapt(dc, opts = {}) {
const shotsDir = opts.shotsDir || './outputs/shots/';
const timeline = [];
for (const s of dc.shots) {
const framing = FRAMING_ZOOM[s.framing] || FRAMING_ZOOM['中景'];
const trim = s.duration || intensityToTrim(s.emotion?.intensity || 5);
const shotEntry = {
shot: `${shotsDir}${dc.project}-${dc.episode}-${s.id}.mp4`,
trim,
transition: s.transition || rhythmToTransition(s.rhythm),
keyframes: s.keyframes || {},
};
// 景别映射到 keyframes.zoom如果导演没有显式指定
if (!shotEntry.keyframes.zoom && framing.from !== framing.to) {
shotEntry.keyframes.zoom = framing;
}
timeline.push(shotEntry);
}
// 取主导情绪的调色参数
const mainEmotion = dc.emotion_main || '悬';
const colorGrade = EMOTION_COLOR[mainEmotion] || EMOTION_COLOR['悬'];
const result = {
timeline,
colorGrade,
resolution: opts.resolution || { w: 1920, h: 1080 },
fps: opts.fps || 24,
output: opts.output || `./outputs/${dc.project}-${dc.episode}-final.mp4`,
};
if (opts.voiceFile) {
result.audio = { ...(result.audio || {}), voice: opts.voiceFile };
}
if (opts.bgmFile) {
result.audio = { ...(result.audio || {}), bgm: opts.bgmFile, bgmVolume: 0.3 };
}
if (opts.srtFile) {
result.subtitle = opts.srtFile;
}
// 附加元信息
result._meta = {
project: dc.project,
episode: dc.episode,
tempo: dc.tempo,
emotion_main: mainEmotion,
hooks: dc.hooks,
shotCount: timeline.length,
totalDuration: timeline.reduce((sum, t) => sum + t.trim, 0),
};
return result;
}
module.exports = { adapt, FRAMING_ZOOM, EMOTION_COLOR, intensityToTrim, rhythmToTransition };