diff --git a/video-ai-system/engines/hldp-prompt.js b/video-ai-system/engines/hldp-prompt.js new file mode 100644 index 0000000..0c09c67 --- /dev/null +++ b/video-ai-system/engines/hldp-prompt.js @@ -0,0 +1,307 @@ +/** + * 光湖视频AI系统 · HLDP→Seedance提示词转换器 + * 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, +};