2026-06-11 20:26:21 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 光湖视频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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-12 23:17:59 +08:00
|
|
|
|
// ==================== 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,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 20:26:21 +08:00
|
|
|
|
module.exports = {
|
|
|
|
|
|
sceneToPrompts,
|
|
|
|
|
|
understandScene,
|
|
|
|
|
|
episodeToStoryboard,
|
2026-06-12 23:17:59 +08:00
|
|
|
|
// D131 新增: HLDP编号协议展开引擎
|
|
|
|
|
|
expand,
|
|
|
|
|
|
validate,
|
|
|
|
|
|
expandAndValidate,
|
|
|
|
|
|
getDBStatus,
|
2026-06-11 20:26:21 +08:00
|
|
|
|
};
|