From befc4e9d5f30a06efb917fa0347d01fda89112b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Mon, 15 Jun 2026 15:02:35 +0800 Subject: [PATCH] =?UTF-8?q?D132:=20hldp-prompt.js=20=E7=BC=96=E5=8F=B7?= =?UTF-8?q?=E5=B1=95=E5=BC=80=E5=BC=95=E6=93=8E=E4=BF=AE=E5=A4=8D=20=C2=B7?= =?UTF-8?q?=20=E8=A1=A3=E6=9C=8D=E5=B1=82=E6=AD=A3=E5=BC=8F=E8=90=BD?= =?UTF-8?q?=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复: 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编号展开 = 同一人物视觉 --- video-ai-system/engines/hldp-prompt.js | 205 +++++++++++-------------- 1 file changed, 86 insertions(+), 119 deletions(-) diff --git a/video-ai-system/engines/hldp-prompt.js b/video-ai-system/engines/hldp-prompt.js index be95fe4..d5a9031 100644 --- a/video-ai-system/engines/hldp-prompt.js +++ b/video-ai-system/engines/hldp-prompt.js @@ -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,