冰朔 485d663ceb
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled
D131: HLDP编号协议系统 · characters.hdlp + environments.hdlp + 展开引擎expand/validate + WHY推理链
2026-06-12 23:17:59 +08:00

489 lines
16 KiB
JavaScript
Raw 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编号协议 · 展开引擎 ====================
// D131 · 铸渊 ICE-GL-ZY001
// 核心公式: CHAR-001 = 同一段文本 → 每次API调用传同一段文本 → Seedance输出更接近一致
// 防幻觉: 展开后做字符串比对,任何偏差都切回数据库原文
const fs = require('fs');
const path = require('path');
// 运行时数据库(模块加载时解析)
const DB = { characters: {}, environments: {} };
/**
* 解析HLDP格式的数据库文件
* 提取每个CHAR/ENV编号对应的完整提示词片段
*/
function loadDatabase() {
const dataDir = path.resolve(__dirname, '../data');
// 解析人物库
try {
const charContent = fs.readFileSync(path.join(dataDir, 'characters.hdlp'), 'utf-8');
const charEntries = charContent.split(/## CHAR-\d+/).slice(1);
for (const entry of charEntries) {
const match = entry.match(/CHAR-(\d+)/);
if (!match) continue;
const id = `CHAR-${match[1]}`;
const promptMatch = entry.match(/提示词片段:\s*(.+?)(?:\n|$)/);
if (promptMatch) {
DB.characters[id] = promptMatch[1].trim();
}
}
} catch (e) { /* 数据库文件不存在时跳过 */ }
// 解析环境库
try {
const envContent = fs.readFileSync(path.join(dataDir, 'environments.hdlp'), 'utf-8');
const envEntries = envContent.split(/## ENV-\d+/).slice(1);
for (const entry of envEntries) {
const match = entry.match(/ENV-(\d+)/);
if (!match) continue;
const id = `ENV-${match[1]}`;
const promptMatch = entry.match(/提示词片段:\s*(.+?)(?:\n|$)/);
if (promptMatch) {
DB.environments[id] = promptMatch[1].trim();
}
}
} catch (e) { /* 数据库文件不存在时跳过 */ }
}
// 模块加载时装载数据库
loadDatabase();
/**
* 展开编号为完整提示词描述
* [CHAR-001] → "33岁亚洲男性方脸浓眉..."
* [ENV-001] → "科幻飞船底层船员宿舍约20平米..."
*
* @param {string} text - 包含编号引用的文本
* @returns {object} { expanded: 展开后文本, warnings: 未找到的编号列表 }
*/
function expand(text) {
const warnings = [];
let result = text;
// 展开CHAR编号
result = result.replace(/\[CHAR-(\d+)\]/g, (match, num) => {
const id = `CHAR-${num}`;
const expanded = DB.characters[id];
if (!expanded) {
warnings.push(`未知编号: ${id}`);
return match;
}
return expanded;
});
// 展开ENV编号
result = result.replace(/\[ENV-(\d+)\]/g, (match, num) => {
const id = `ENV-${num}`;
const expanded = DB.environments[id];
if (!expanded) {
warnings.push(`未知编号: ${id}`);
return match;
}
return expanded;
});
return { expanded: result, warnings };
}
/**
* 校验:确保展开后不会产生数据丢失
* 铸渊的承诺:同一个编号,同一个展开结果。
* 如果展开后的文本里某一处偏离了数据库原文,记录并报告。
*
* @param {string} original - 原始编号引用文本
* @param {string} expanded - 展开后文本
* @returns {object} { valid: boolean, diffs: [] }
*/
function validate(original, expanded) {
const diffs = [];
// 提取原始文本中的所有CHAR编号
const charRefs = [...original.matchAll(/\[CHAR-(\d+)\]/g)];
for (const ref of charRefs) {
const id = `CHAR-${ref[1]}`;
const dbText = DB.characters[id];
if (dbText && !expanded.includes(dbText)) {
diffs.push({
id,
expected: dbText.substring(0, 60) + '...',
issue: '展开后的提示词中未找到此编号的数据库原文',
});
}
}
// 提取原始文本中的所有ENV编号
const envRefs = [...original.matchAll(/\[ENV-(\d+)\]/g)];
for (const ref of envRefs) {
const id = `ENV-${ref[1]}`;
const dbText = DB.environments[id];
if (dbText && !expanded.includes(dbText)) {
diffs.push({
id,
expected: dbText.substring(0, 60) + '...',
issue: '展开后的提示词中未找到此编号的数据库原文',
});
}
}
return { valid: diffs.length === 0, diffs };
}
/**
* 展开并校验的便捷函数
* 铸渊在生成提示词时调用此函数。
* 如果校验不通过,使用数据库原文逐个替换,强制一致性。
*
* @param {string} text - 包含编号引用的文本
* @returns {string} 最终的Seedance提示词
*/
function expandAndValidate(text) {
const { expanded, warnings } = expand(text);
if (!warnings.length && DB.characters && DB.environments) {
// 防幻觉: 如果展开后的文本里缺少任何数据库原文,逐个检查并替换
let final = expanded;
const allRefs = [...text.matchAll(/\[(CHAR|ENV)-(\d+)\]/g)];
for (const ref of allRefs) {
const type = ref[1];
const num = ref[2];
const id = `${type}-${num}`;
const dbText = type === 'CHAR' ? DB.characters[id] : DB.environments[id];
if (dbText && !final.includes(dbText)) {
// 强制替换: 把展开后的不完整片段替换为数据库原文
final = final.replace(new RegExp(`\\[${type}-${num}\\]`), dbText);
// 如果已经被展开过但没有匹配上,说明展开结果有问题,直接追加
if (!final.includes(dbText)) {
final += ' ' + dbText;
}
warnings.push(`⚠️ 编号${id}展开后与原文不一致,已强制切回数据库原文`);
}
}
return final;
}
return expanded;
}
// 导出数据库状态,供外部查询
function getDBStatus() {
return {
characters: Object.keys(DB.characters).length,
environments: Object.keys(DB.environments).length,
};
}
module.exports = {
sceneToPrompts,
understandScene,
episodeToStoryboard,
// D131 新增: HLDP编号协议展开引擎
expand,
validate,
expandAndValidate,
getDBStatus,
};