D132: hldp-prompt.js 编号展开引擎修复 · 衣服层正式落地
修复:
loadDatabase() 改用 matchAll 直接捕获CHAR/ENV编号+提示词片段
原D131版本用split()导致编号被丢弃,DB始终为空 → 秦山号翻车根因
新增:
expandPrompt(charId, envId, action, style) — 一键缝衣服
用法: expandPrompt('CHAR-003','ENV-002','叹气','真人写实')
→ 自动展开CHAR+ENV+动作+风格后缀 → 完整Seedance提示词
验证通过: CHAR-003苏白+ENV-002百宗会+真人写实 → 完整提示词输出正确
CHAR-001林昊+ENV-001秦山号+真人写实 → 同样正确
HLDP记录: D131翻车 → D132修复 → 同一CHAR编号展开 = 同一人物视觉
This commit is contained in:
parent
ef144ec729
commit
befc4e9d5f
@ -301,186 +301,153 @@ function extractEpisodeChars(episode) {
|
||||
}
|
||||
|
||||
// ==================== HLDP编号协议 · 展开引擎 ====================
|
||||
// D131 · 铸渊 ICE-GL-ZY001
|
||||
// 核心公式: CHAR-001 = 同一段文本 → 每次API调用传同一段文本 → Seedance输出更接近一致
|
||||
// 防幻觉: 展开后做字符串比对,任何偏差都切回数据库原文
|
||||
// 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编号对应的完整提示词片段
|
||||
* 解析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 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();
|
||||
}
|
||||
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) { /* 数据库文件不存在时跳过 */ }
|
||||
} 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();
|
||||
}
|
||||
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) { /* 数据库文件不存在时跳过 */ }
|
||||
} catch (e) { /* 数据库文件不存在 */ }
|
||||
}
|
||||
|
||||
// 模块加载时装载数据库
|
||||
loadDatabase();
|
||||
|
||||
/**
|
||||
* 展开编号为完整提示词描述
|
||||
* [CHAR-001] → "33岁亚洲男性,方脸浓眉..."
|
||||
* [ENV-001] → "科幻飞船底层船员宿舍,约20平米..."
|
||||
* 展开编号模板 → 完整Seedance提示词。
|
||||
*
|
||||
* @param {string} text - 包含编号引用的文本
|
||||
* @returns {object} { expanded: 展开后文本, warnings: 未找到的编号列表 }
|
||||
* 用法: 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;
|
||||
|
||||
// 展开CHAR编号
|
||||
result = result.replace(/\[CHAR-(\d+)\]/g, (match, num) => {
|
||||
result = result.replace(/\[CHAR-(\d+)\]/g, (m, num) => {
|
||||
const id = `CHAR-${num}`;
|
||||
const expanded = DB.characters[id];
|
||||
if (!expanded) {
|
||||
warnings.push(`未知编号: ${id}`);
|
||||
return match;
|
||||
}
|
||||
return expanded;
|
||||
return DB.characters[id] || (warnings.push(`未知: ${id}`), m);
|
||||
});
|
||||
|
||||
// 展开ENV编号
|
||||
result = result.replace(/\[ENV-(\d+)\]/g, (match, num) => {
|
||||
result = result.replace(/\[ENV-(\d+)\]/g, (m, num) => {
|
||||
const id = `ENV-${num}`;
|
||||
const expanded = DB.environments[id];
|
||||
if (!expanded) {
|
||||
warnings.push(`未知编号: ${id}`);
|
||||
return match;
|
||||
}
|
||||
return expanded;
|
||||
return DB.environments[id] || (warnings.push(`未知: ${id}`), m);
|
||||
});
|
||||
|
||||
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) {
|
||||
for (const ref of [...original.matchAll(/\[CHAR-(\d+)\]/g)]) {
|
||||
const id = `CHAR-${ref[1]}`;
|
||||
const dbText = DB.characters[id];
|
||||
if (dbText && !expanded.includes(dbText)) {
|
||||
diffs.push({
|
||||
id,
|
||||
expected: dbText.substring(0, 60) + '...',
|
||||
issue: '展开后的提示词中未找到此编号的数据库原文',
|
||||
});
|
||||
}
|
||||
if (DB.characters[id] && !expanded.includes(DB.characters[id]))
|
||||
diffs.push({ id, issue: '展开结果中缺失数据库原文' });
|
||||
}
|
||||
|
||||
// 提取原始文本中的所有ENV编号
|
||||
const envRefs = [...original.matchAll(/\[ENV-(\d+)\]/g)];
|
||||
for (const ref of envRefs) {
|
||||
for (const ref of [...original.matchAll(/\[ENV-(\d+)\]/g)]) {
|
||||
const id = `ENV-${ref[1]}`;
|
||||
const dbText = DB.environments[id];
|
||||
if (dbText && !expanded.includes(dbText)) {
|
||||
diffs.push({
|
||||
id,
|
||||
expected: dbText.substring(0, 60) + '...',
|
||||
issue: '展开后的提示词中未找到此编号的数据库原文',
|
||||
});
|
||||
}
|
||||
if (DB.environments[id] && !expanded.includes(DB.environments[id]))
|
||||
diffs.push({ id, 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}展开后与原文不一致,已强制切回数据库原文`);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
return expanded;
|
||||
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,
|
||||
// D131 新增: HLDP编号协议展开引擎
|
||||
expandPrompt,
|
||||
expand,
|
||||
validate,
|
||||
expandAndValidate,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user