1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-20 21:06:48 +08:00
parent 1c72066bab
commit 75944b1f1f
102 changed files with 9648 additions and 1540 deletions

View File

@@ -7,20 +7,17 @@ import {
/**
* 角色资产正式 prompt 主源。
*
* 这份脚本同时承担两层职责:
* 1. 角色卡 -> 默认资产描述文本
* - 产出 visualPromptText / animationPromptText / scenePromptText
* - 这层本质上是在“编译默认描述文本”,不是最终直接发给图像模型的完整 prompt
* 2. 默认描述文本 -> 正式模型 prompt
* - buildNpcVisualPrompt / buildNpcAnimationPrompt / buildArkCharacterAnimationPrompt
* - 这层才是正式发给图像 / 动作模型的 prompt 组装入口
* 这份脚本当前只承担“正式模型 prompt 层”职责:
* - buildNpcVisualPrompt
* - buildNpcAnimationPrompt
* - buildArkCharacterAnimationPrompt
* - buildImageSequencePrompt
*
* 当前仓库状态需要特别区分:
* - 当前自定义世界角色资产工坊默认输入框,实际直接使用前端
* src/prompts/customWorldRolePromptDefaults.ts
* - 本文件里的 CHARACTER_PROMPT_BUNDLE_SYSTEM_PROMPT 及其生成接口
* /api/assets/character-prompts/generate 目前仍保留、可用、且有测试覆盖,
* 但不是当前资产工坊初始默认值的主链来源
* - 默认描述文本的唯一主源已经统一为前端本地映射,
* 不再保留后端独立 bundle 编译接口
* - 当前正式角色主图与动作生成,仍然走本文件里的正式 prompt builder
*/
function clampPromptSeedText(value: unknown, maxLength: number) {
@@ -31,147 +28,6 @@ function clampPromptSeedText(value: unknown, maxLength: number) {
return value.replace(/\s+/gu, ' ').trim().slice(0, maxLength);
}
export const CHARACTER_PROMPT_BUNDLE_SYSTEM_PROMPT = `你是 RPG 角色资产提示词编译器。
你会收到一个角色设定摘要,请为当前项目生成 3 段可直接交给资产生成模型的中文提示词。
你必须只输出一个 JSON 对象,不要输出 Markdown、代码块、注释或解释。
输出格式必须严格为:
{
"visualPromptText": "角色主图提示词",
"animationPromptText": "角色动作提示词",
"scenePromptText": "角色关联场景提示词"
}
硬性约束:
- 所有字段都必须是自然中文。
- visualPromptText 用于角色主图候选必须是角色标准设定图而不是场景海报突出单人全身、右向斜侧身站姿、脚底完整可见、服装武器轮廓稳定、纯绿色绿幕背景、1:1 画幅。
- visualPromptText 里的主题词只能落在角色自身的服装、发型、材质、纹样、饰品、武器和发光细节上,不要自动补出建筑、风景、漂浮物、烟雾或其他角色以外的场景元素。
- visualPromptText 要明确“身体整体朝右,但保留少量正面信息”,避免生成完全 90 度纯右视图。
- animationPromptText 用于角色动作试片,必须突出发力方式、动作气质、连贯性、同一角色一致性,不要写镜头切换。
- scenePromptText 用于该角色关联的场景背景,必须突出角色首次登场或主活动区域的环境气质与空间结构,适配横版 RPG 场景。
- 三段提示词都要可直接使用,不要编号,不要加字段名解释,不要输出负面提示词。`;
export type CharacterPromptBundle = {
visualPromptText: string;
animationPromptText: string;
scenePromptText: string;
source: 'llm' | 'fallback';
model: string | null;
};
/**
* 当默认描述文本编译接口不可用,或当前环境不走 LLM 编译时,
* 用角色卡字段本地拼出一份可直接使用的默认文本 bundle。
*
* 这份返回值属于“默认描述文本层”:
* - visualPromptText: 给角色主图用的默认描述
* - animationPromptText: 给动作试片用的默认描述
* - scenePromptText: 给角色关联场景用的默认描述
*
* 它不是最终发给正式图像 / 动作模型的完整 prompt。
*/
export function buildFallbackCharacterPromptBundle(params: {
characterName: string;
roleKind: string;
roleTitle: string;
roleLabel: string;
description: string;
backstory: string;
personality: string;
motivation: string;
combatStyle: string;
tags: string[];
}) {
const roleAnchor =
[params.roleTitle, params.roleLabel].filter(Boolean).join(' / ') ||
(params.roleKind === 'playable' ? '可扮演角色' : '场景角色');
const characterAnchor = params.characterName || '该角色';
const descriptionAnchor =
params.description || params.backstory || params.personality || '气质鲜明';
const combatAnchor =
params.combatStyle || params.motivation || '动作发力清晰';
const tagAnchor =
params.tags.length > 0 ? `保留 ${params.tags.join('、')} 的识别点。` : '';
return {
visualPromptText: [
`${characterAnchor}${roleAnchor}`,
'单人全身2D 横版 RPG 角色标准设定图1:1 正方形画幅,头身比控制在 2 到 2.5 头身,偏大头身,靠头部、发型、服装、配饰表现角色记忆点,躯干与四肢短而紧凑,五官简化,深色粗轮廓配合清晰大色块,右向斜侧身站立,身体整体朝右但保留少量正面信息,服装、发型、轮廓稳定清楚。',
`外观气质围绕:${descriptionAnchor}`,
combatAnchor ? `战斗识别点:${combatAnchor}` : '',
tagAnchor,
'背景固定为纯绿色绿幕,不带建筑、风景、漂浮物和其他场景元素,方便自动抠像,不做正面立绘,不做完全 90 度纯右视图,不做夸张透视。',
]
.filter(Boolean)
.join(' '),
animationPromptText: [
`${characterAnchor}的核心动作试片。`,
'保持同一角色的服装、发型、体型一致,镜头稳定,侧身朝右,动作连贯。',
combatAnchor ? `动作气质参考:${combatAnchor}` : '',
params.personality ? `角色气质补充:${params.personality}` : '',
'发力起手明确,过程干净,收招利落,避免漂移和变形。',
]
.filter(Boolean)
.join(' '),
scenePromptText: [
`${characterAnchor}关联主场景,适合作为首次登场区域或常驻活动空间。`,
'16:9 横版 RPG 场景背景,上下分区清楚,上半部分表现中远景氛围,下半部分是可站立地面。',
`场景叙事气质围绕:${descriptionAnchor}`,
params.backstory ? `背景线索可参考:${params.backstory}` : '',
params.motivation
? `环境中可埋入与当前目标相关的暗示:${params.motivation}`
: '',
'整体风格克制统一,适合剧情探索与战斗底图。',
]
.filter(Boolean)
.join(' '),
source: 'fallback' as const,
model: null,
};
}
function sanitizePromptBundleValue(
value: unknown,
fallback: string,
maxLength: number,
) {
const normalized = clampPromptSeedText(value, maxLength);
return normalized || fallback;
}
/**
* 将 LLM 返回的默认文本 bundle 规整成稳定结构。
*
* 这里只负责兜底、限长和字段补齐,不负责把 bundle 进一步编译成
* 正式图像 / 动作生成 prompt。
*/
export function sanitizeCharacterPromptBundle(
value: unknown,
fallback: CharacterPromptBundle,
model: string,
) {
const record = isRecordValue(value) ? value : {};
return {
visualPromptText: sanitizePromptBundleValue(
record.visualPromptText,
fallback.visualPromptText,
280,
),
animationPromptText: sanitizePromptBundleValue(
record.animationPromptText,
fallback.animationPromptText,
280,
),
scenePromptText: sanitizePromptBundleValue(
record.scenePromptText,
fallback.scenePromptText,
320,
),
source: 'llm' as const,
model: model.trim() || null,
};
}
function sanitizeAnimationPromptText(value: string, maxLength: number) {
return value
.replace(/\s+/gu, ' ')
@@ -197,48 +53,6 @@ function buildCompactAnimationCharacterBrief(value: string) {
.join('');
}
/**
* 默认文本 bundle 的 user prompt。
*
* 这段文本只用于让 LLM 从角色卡摘要里提炼出
* visualPromptText / animationPromptText / scenePromptText 三段默认描述,
* 不是正式图像模型或动作模型的 system prompt。
*/
export function buildCharacterPromptBundleUserPrompt(params: {
roleKind: string;
characterBriefText: string;
characterName: string;
roleTitle: string;
roleLabel: string;
description: string;
backstory: string;
personality: string;
motivation: string;
combatStyle: string;
tags: string[];
}) {
return [
'请根据下面的角色卡摘要,编译一组默认资产提示词。',
'提示词用于当前项目的角色主图、动作试片和角色关联场景背景。',
'请保留该角色的身份识别点、气质、战斗方式与世界感,不要空泛套模板。',
'',
`角色类型:${params.roleKind === 'playable' ? '可扮演角色' : '场景角色'}`,
params.characterName ? `角色名称:${params.characterName}` : '',
params.roleTitle ? `角色头衔:${params.roleTitle}` : '',
params.roleLabel ? `世界身份:${params.roleLabel}` : '',
params.description ? `角色描述:${params.description}` : '',
params.backstory ? `角色背景:${params.backstory}` : '',
params.personality ? `角色性格:${params.personality}` : '',
params.motivation ? `角色动机:${params.motivation}` : '',
params.combatStyle ? `战斗风格:${params.combatStyle}` : '',
params.tags.length > 0 ? `角色标签:${params.tags.join('、')}` : '',
'',
'角色卡全文:',
params.characterBriefText,
]
.filter(Boolean)
.join('\n');
}
/**
* 正式角色主图 prompt 编译入口。

View File

@@ -212,6 +212,34 @@ function describeNpcConversationHistory(history: unknown, npcName: string) {
: '当前聊天记录:暂无。';
}
function describeNpcCombatContext(combatContext: unknown) {
const record = asRecord(combatContext);
const summary = readString(record?.summary);
const battleOutcome = readString(record?.battleOutcome);
const logLines = readStringArray(record?.logLines).slice(0, 6);
if (!summary && logLines.length === 0) {
return null;
}
const outcomeText =
battleOutcome === 'spar_complete'
? '切磋刚刚结束。'
: battleOutcome === 'victory'
? '战斗刚刚分出胜负。'
: null;
return [
'刚刚结束的交锋:',
outcomeText,
summary ? `- 结果摘要:${summary}` : null,
...(logLines.length > 0
? ['- 战斗日志:', ...logLines.map((line) => ` - ${line}`)]
: []),
]
.filter(Boolean)
.join('\n');
}
function describeSceneContext(context: unknown) {
const record = asRecord(context);
const sceneName = readString(record?.sceneName) ?? '当前区域';
@@ -510,10 +538,12 @@ export function buildNpcChatTurnReplyPrompt(
context?.firstContactRelationStance,
);
const playerMessage = payload.playerMessage.trim();
const combatContextBlock = describeNpcCombatContext(payload.combatContext);
return [
buildNpcDialoguePromptBase(payload),
describeNpcConversationHistory(conversationHistory, encounter.npcName),
combatContextBlock,
openingCampBackground ? `营地开场背景:${openingCampBackground}` : null,
openingCampDialogue ? `刚刚发生的第一段对话:${openingCampDialogue}` : null,
`当前关系值:${affinity}`,
@@ -574,10 +604,12 @@ export function buildNpcChatTurnSuggestionPrompt(
Array.isArray(payload.conversationHistory) && payload.conversationHistory.length > 0
? payload.conversationHistory
: payload.dialogue ?? payload.conversationHistory ?? [];
const combatContextBlock = describeNpcCombatContext(payload.combatContext);
return [
buildNpcDialoguePromptBase(payload),
describeNpcConversationHistory(conversationHistory, encounter.npcName),
combatContextBlock,
`玩家刚刚说:${payload.playerMessage}`,
`NPC 刚刚回复:${npcReply}`,
`请围绕刚刚这轮对话,为玩家生成 3 条下一轮可以直接说出口的中文接话短句。`,