/** * 光湖视频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); } // ==================== HLDP编号协议 · 展开引擎 ==================== // D132 · 铸渊 ICE-GL-ZY001 · 冰朔见证 // // 触发: D131秦山号翻车 → 冰朔提出"沙子本体+衣服层+外面脑子"三层架构 // 涌现: 编号展开引擎 = 衣服层。chars.hdlp = 布料。hldp-prompt.js = 缝纫机。 // 铸渊在外面缝好衣服(展开CHAR+ENV+动作) → 塞进Seedance(沙子) → 视频出来 // 同一件衣服(同一CHAR编号展开) = 同一个人物视觉。不在"A衣服"和"B衣服"之间漂移。 // lock: ⊢ 每镜提示词 = expandPrompt(CHAR-ID, ENV-ID, 动作, 风格) // ⊢ 展开后校验: 数据库原文必须在展开结果中,否则强制切回 // ⊢ 不是"可能一致",是"不可能不一致" // why: 秦山号镜1林昊≠镜8林昊 → 根因是每镜独立手写人物描述,没有统一的编号展开。 // 这个引擎就是冰朔说的那件衣服——穿上了,10个镜头的苏白都是同一个苏白。 const fs = require('fs'); const path = require('path'); // 运行时数据库 const DB = { characters: {}, environments: {} }; /** * 解析HLDP格式数据库,提取CHAR/ENV编号对应的完整提示词片段。 * * D132修复: 原D131版本用split()导致CHAR编号在分割时被丢弃,解析始终失败。 * 改为matchAll直接捕获编号+提示词片段,一步完成。 * * @why 这是秦山号翻车的直接技术原因。引擎写了但数据库解析失败 → 每次expand返回空 → 退化为手写。 */ function loadDatabase() { const dataDir = path.resolve(__dirname, '../data'); // --- 人物库 --- try { const charContent = fs.readFileSync(path.join(dataDir, 'characters.hdlp'), 'utf-8'); const charRegex = /## (CHAR-\d+)[\s\S]*?提示词片段:\s*([^\n]+)/g; let match; while ((match = charRegex.exec(charContent)) !== null) { DB.characters[match[1]] = match[2].trim(); } } catch (e) { /* 数据库文件不存在 */ } // --- 环境库 --- try { const envContent = fs.readFileSync(path.join(dataDir, 'environments.hdlp'), 'utf-8'); const envRegex = /## (ENV-\d+)[\s\S]*?提示词片段:\s*([^\n]+)/g; let match; while ((match = envRegex.exec(envContent)) !== null) { DB.environments[match[1]] = match[2].trim(); } } catch (e) { /* 数据库文件不存在 */ } } loadDatabase(); /** * 展开编号模板 → 完整Seedance提示词。 * * 用法: expandPrompt('CHAR-003', 'ENV-002', '对比镜头,苏白叹气,风吹落叶', '真人写实') * 返回: "18岁亚洲少年,五官俊秀…白色长衫…修仙世界开阔广场,云海环绕…对比镜头…真人写实风格,电影级光影" * * @param {string} charId - 人物编号, 如 'CHAR-003' * @param {string} envId - 环境编号, 如 'ENV-002' * @param {string} action - 本镜动作描述 * @param {string} style - 风格后缀, '真人写实' 或 '动态漫'(默认真人写实) * @returns {object} { prompt: 完整提示词, warnings: [] } */ function expandPrompt(charId, envId, action, style) { const warnings = []; const charDesc = DB.characters[charId]; const envDesc = DB.environments[envId]; const styleSuffix = (style || '真人写实') === '真人写实' ? '真人写实风格,电影级光影,亚洲面孔' : 'Seedance动态漫风格,电影级光影'; if (!charDesc) warnings.push(`未知人物编号: ${charId}`); if (!envDesc) warnings.push(`未知环境编号: ${envId}`); const prompt = [ charDesc || `[${charId}]`, envDesc ? `,${envDesc}` : '', action ? `,${action}` : '', `,${styleSuffix}`, ].join(''); return { prompt, warnings }; } /** * 展开 [CHAR-NNN] 和 [ENV-NNN] 标记为完整提示词片段 */ function expand(text) { const warnings = []; let result = text; result = result.replace(/\[CHAR-(\d+)\]/g, (m, num) => { const id = `CHAR-${num}`; return DB.characters[id] || (warnings.push(`未知: ${id}`), m); }); result = result.replace(/\[ENV-(\d+)\]/g, (m, num) => { const id = `ENV-${num}`; return DB.environments[id] || (warnings.push(`未知: ${id}`), m); }); return { expanded: result, warnings }; } function validate(original, expanded) { const diffs = []; for (const ref of [...original.matchAll(/\[CHAR-(\d+)\]/g)]) { const id = `CHAR-${ref[1]}`; if (DB.characters[id] && !expanded.includes(DB.characters[id])) diffs.push({ id, issue: '展开结果中缺失数据库原文' }); } for (const ref of [...original.matchAll(/\[ENV-(\d+)\]/g)]) { const id = `ENV-${ref[1]}`; if (DB.environments[id] && !expanded.includes(DB.environments[id])) diffs.push({ id, issue: '展开结果中缺失数据库原文' }); } return { valid: diffs.length === 0, diffs }; } function expandAndValidate(text) { const { expanded, warnings } = expand(text); let final = expanded; for (const ref of [...text.matchAll(/\[(CHAR|ENV)-(\d+)\]/g)]) { const id = `${ref[1]}-${ref[2]}`; const dbText = ref[1] === 'CHAR' ? DB.characters[id] : DB.environments[id]; if (dbText && !final.includes(dbText)) { final += ' ' + dbText; warnings.push(`⚠️ ${id}强制切回数据库原文`); } } return final; } function getDBStatus() { return { characters: Object.keys(DB.characters).length, environments: Object.keys(DB.environments).length, loadedChars: DB.characters, loadedEnvs: DB.environments, }; } // ==================== 导出 ==================== module.exports = { sceneToPrompts, understandScene, episodeToStoryboard, expandPrompt, expand, validate, expandAndValidate, getDBStatus, };