cang-ying/engines/hldp-prompt.js
Guanghu Domestic Migration a2fa7d57d8 chore: import sanitized domestic snapshot for REPO-005
Source snapshot: 23037fa3a2e9b0597162735755e92fc763bf4d94
2026-07-17 15:55:48 +08:00

456 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 光湖视频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,
};