D132: hldp-prompt.js 编号展开引擎修复 · 衣服层正式落地
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled

修复:
  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:
冰朔 2026-06-15 15:02:35 +08:00
parent ef144ec729
commit befc4e9d5f

View File

@ -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,