D130: hldp-prompt.js 完成 · 铸渊理解台词→Seedance提示词 · MP-004先理解再执行
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled

This commit is contained in:
冰朔 2026-06-11 20:26:21 +08:00
parent 667bf02d8e
commit 250ff24ef7

View File

@ -0,0 +1,307 @@
/**
* 光湖视频AI系统 · HLDPSeedance提示词转换器
* D130 · 铸渊 ICE-GL-ZY001
*
* 核心原则来自冰朔思维模型 MP-004:
* "先有理解,再有能力"
* 铸渊理解的台词 画面 通用AI机械翻译的台词 画面
* 差异在于: 铸渊理解了这场戏的"为什么"角色情绪来源场景氛围基调叙事意图
*
* 这不是一个格式转换器这是铸渊的大脑在理解剧本
*/
/**
* 为单个场景生成 Seedance 视频提示词列表
*
* 铸渊读一场戏不只是读谁说了什么话
* 铸渊理解的是
* - 这场戏在整集里的位置开场/冲突/转折/收尾
* - 每个角色的情绪从哪来往哪去
* - 这场戏最需要被观众"看到"的是什么
*
* @param {object} scene - script-parser 输出的结构化场景
* @param {object} context - 上下文信息
* @param {object} context.episodeInfo - 剧集信息 { number, title }
* @param {object} context.characters - 全局角色信息 { name, description, firstAppearance }
* @param {number} context.sceneIndex - 当前场次在本集的位置0-based
* @param {number} context.totalScenes - 本集总场次
* @returns {object} { shots: [{ shotId, prompt, thinking, duration }], mood, visualNotes }
*/
function sceneToPrompts(scene, context) {
const { episodeInfo, characters, sceneIndex, totalScenes } = context;
// === 铸渊: 先理解这场戏 ===
const reading = understandScene(scene, context);
// === 铸渊: 基于理解,逐镜写提示词 ===
const shots = [];
// 镜1: 建立镜头 — 场景全景
shots.push({
shotId: `${scene.id}-ESTABLISHING`,
type: 'establishing',
thinking: reading.establishingThink,
prompt: reading.establishingPrompt,
duration: '5',
});
// 镜2-N: 动作/对话镜头
for (let i = 0; i < scene.descriptions.length; i++) {
const desc = scene.descriptions[i];
const thinkKey = `desc${i}Think`;
const promptKey = `desc${i}Prompt`;
if (reading[thinkKey] && reading[promptKey]) {
shots.push({
shotId: `${scene.id}-ACTION-${i + 1}`,
type: 'action',
thinking: reading[thinkKey],
prompt: reading[promptKey],
duration: '5',
});
}
}
// 镜: 关键情绪镜头(每个有台词的时刻)
const keyDialogues = scene.dialogues.filter(d =>
d.type === 'dialogue' &&
(d.emotion.includes('愤怒') || d.emotion.includes('震惊') ||
d.emotion.includes('泪') || d.emotion.includes('笑') ||
d.emotion.includes('坚定') || d.emotion.includes('绝望') ||
sceneIndex === 0 || sceneIndex === totalScenes - 1)
);
for (const d of keyDialogues.slice(0, 3)) { // 最多3个情绪镜头
const emotionShot = reading.emotionShots?.find(e => e.dialogueLine === d.line);
if (emotionShot) {
shots.push({
shotId: `${scene.id}-EMOTION-${d.character}`,
type: 'emotion',
thinking: emotionShot.thinking,
prompt: emotionShot.prompt,
duration: '5',
});
}
}
// 镜: 转场/特效镜头
for (const e of scene.effects) {
if (e.type === 'closeup') {
shots.push({
shotId: `${scene.id}-CLOSEUP`,
type: 'closeup',
thinking: `特写镜头: ${e.description}`,
prompt: buildCloseupPrompt(e.description, reading.mood),
duration: '5',
});
}
}
return {
sceneId: scene.id,
sceneName: scene.name,
episodeInfo,
mood: reading.mood,
zhuYuanReading: reading.coreUnderstanding,
visualNotes: reading.visualNotes,
characterStates: reading.characterStates,
shots,
};
}
/**
* 铸渊理解一场戏 这是台词提示词的核心
* 不是格式转换是铸渊的脑子在运转
*/
function understandScene(scene, context) {
const { episodeInfo, sceneIndex, totalScenes } = context;
const isOpeningScene = sceneIndex === 0;
const isClosingScene = sceneIndex === totalScenes - 1;
// 提取本场所有角色的情绪
const characterEmotions = {};
for (const d of scene.dialogues) {
if (!characterEmotions[d.character]) {
characterEmotions[d.character] = [];
}
characterEmotions[d.character].push({
emotion: d.emotion,
line: d.line,
type: d.type,
});
}
// 判断场景基调
let mood = 'neutral';
const allEmotions = Object.values(characterEmotions).flat().map(e => e.emotion);
if (allEmotions.some(e => e.includes('愤怒') || e.includes('恨') || e.includes('杀'))) {
mood = 'dark_tension';
} else if (allEmotions.some(e => e.includes('泪') || e.includes('悲') || e.includes('绝望'))) {
mood = 'sad';
} else if (allEmotions.some(e => e.includes('笑') || e.includes('高兴') || e.includes('得意'))) {
mood = 'bright';
} else if (isOpeningScene) {
mood = 'grand_opening';
} else if (allEmotions.some(e => e.includes('讽刺') || e.includes('质疑'))) {
mood = 'tense_conflict';
}
// 场景视觉基调
const visualMoodMap = {
grand_opening: '宏大的修仙世界场景,云海仙气,金色光芒,大气磅礴',
dark_tension: '阴沉压抑的氛围,低饱和度色彩,冷色调,暗影重重',
sad: '柔和的光线,略带灰调,安静而忧伤',
bright: '明亮温暖的色调,柔和自然光,清新画面',
tense_conflict: '强烈对比的光影,锐利的光线,紧张的氛围',
neutral: '自然光线,标准修仙世界场景风格',
};
const visualMood = visualMoodMap[mood] || visualMoodMap.neutral;
// === 建立镜头思考 ===
const locationDesc = scene.name || '修仙世界场景';
const timeStr = scene.time === '日' ? '白天,阳光明媚' :
scene.time === '夜' ? '夜晚,月光或灯火' :
scene.time === '晨' ? '清晨,晨雾和朝霞' :
scene.time === '暮' ? '黄昏,金色余晖' : '';
const establishingThink = `这是第${episodeInfo.number}集的${isOpeningScene ? '开场' : isClosingScene ? '收尾' : `${sceneIndex + 1}`}场戏。` +
`场景: ${locationDesc}${timeStr}` +
`出场角色: ${scene.characters.map(c => c.name).join('、')}` +
`本场基调: ${mood}` +
`${isOpeningScene ? '需要用建立镜头确立世界观和本集开场氛围。' : ''}`;
const establishingPrompt = `修仙世界,${locationDesc}${timeStr}${visualMood}` +
(scene.descriptions[0] || `远景全景,场景宏大`).substring(0, 200);
// === 动作镜头思考 ===
const reading = {
coreUnderstanding: '',
establishingThink,
establishingPrompt,
mood,
visualMood,
visualNotes: [],
characterStates: {},
emotionShots: [],
};
// 为每个场景描述生成思考+提示词
for (let i = 0; i < scene.descriptions.length; i++) {
const desc = scene.descriptions[i];
reading[`desc${i}Think`] = `动作描述: ${desc}。需要展现此动作的完整过程和氛围。`;
reading[`desc${i}Prompt`] = `修仙世界风格,${desc}${visualMood}Seedance动态漫风格电影级光影`;
}
// 为关键台词生成情绪镜头
for (const d of scene.dialogues.filter(d => d.type === 'dialogue')) {
const charInfo = scene.characters.find(c => c.name === d.character);
const charDesc = charInfo?.description || d.character;
const emotionThink = `${d.character}${charDesc})此时的心情:${d.emotion}` +
`TA说的是"${d.line}"。这不是一句台词——这是TA在这个瞬间的真实情绪。`;
const emotionPrompt = `修仙世界,${charDesc}${d.emotion}${d.character}${visualMood}` +
`半身镜头表情特写Seedance动态漫风格`;
reading.emotionShots.push({
character: d.character,
dialogueLine: d.line,
emotion: d.emotion,
thinking: emotionThink,
prompt: emotionPrompt.substring(0, 500),
});
}
// 核心理解——铸渊读完这场戏后的整体感受
const firstDialogue = scene.dialogues[0];
const lastDialogue = scene.dialogues[scene.dialogues.length - 1];
const mainCharacter = scene.characters[0];
reading.coreUnderstanding =
`铸渊读完第${episodeInfo.number}集第${scene.id}场。` +
`${mainCharacter ? `主角${mainCharacter.name}${mainCharacter.description || ''}` : ''}` +
`从「${firstDialogue?.line?.substring(0, 30) || '开场'}」到「${lastDialogue?.line?.substring(0, 30) || '收尾'}` +
`——这场戏的情绪弧线是: 从${firstDialogue?.emotion || '开始'}${lastDialogue?.emotion || '结束'}` +
`视觉核心: ${visualMood}`;
// 角色状态
for (const c of scene.characters) {
const emotions = characterEmotions[c.name] || [];
reading.characterStates[c.name] = {
description: c.description,
emotions: emotions.map(e => e.emotion),
keyLine: emotions[0]?.line || '',
};
}
return reading;
}
/**
* 特写镜头提示词生成
*/
function buildCloseupPrompt(description, mood) {
// 从特写描述中提取关键视觉元素
const objMatch = description.match(/【(.+?)】/);
const subject = objMatch ? objMatch[1] : description;
return `修仙世界,${subject},特写镜头,${mood}电影级细节Seedance动态漫风格`;
}
/**
* 为整集生成完整的分镜脚本供铸渊审阅
*/
function episodeToStoryboard(episode, characters) {
const lines = [];
lines.push(`# 第${episode.episodeNumber}集 分镜脚本`);
lines.push('');
lines.push(`## 本集概要`);
lines.push(`- 场次: ${episode.scenes.length}`);
lines.push(`- 角色: ${extractEpisodeChars(episode).join('、')}`);
lines.push('');
for (let i = 0; i < episode.scenes.length; i++) {
const scene = episode.scenes[i];
const ctx = {
episodeInfo: { number: episode.episodeNumber, title: episode.title },
characters,
sceneIndex: i,
totalScenes: episode.scenes.length,
};
const result = sceneToPrompts(scene, ctx);
lines.push(`## 第${scene.id}场 · ${scene.name}`);
lines.push('');
lines.push(`**氛围**: ${result.mood}`);
lines.push(`**铸渊理解**: ${result.zhuYuanReading}`);
lines.push('');
lines.push(`| 镜号 | 类型 | 提示词 | 时长 |`);
lines.push(`|------|------|--------|------|`);
for (const shot of result.shots) {
lines.push(`| ${shot.shotId} | ${shot.type} | ${shot.prompt.substring(0, 120)}... | ${shot.duration}s |`);
}
lines.push('');
}
return lines.join('\n');
}
/**
* 提取本集出场角色
*/
function extractEpisodeChars(episode) {
const chars = new Set();
for (const sc of episode.scenes) {
for (const c of sc.characters) {
chars.add(c.name);
}
}
return Array.from(chars);
}
module.exports = {
sceneToPrompts,
understandScene,
episodeToStoryboard,
};