280 lines
9.0 KiB
TypeScript
280 lines
9.0 KiB
TypeScript
import {
|
||
buildMasterPrompt,
|
||
buildVideoActionPrompt,
|
||
getActionTemplateById,
|
||
} from '../../../packages/shared/src/prompts/qwenSprite.js';
|
||
|
||
/**
|
||
* 角色资产正式 prompt 主源。
|
||
*
|
||
* 这份脚本当前只承担“正式模型 prompt 层”职责:
|
||
* - buildNpcVisualPrompt
|
||
* - buildNpcAnimationPrompt
|
||
* - buildArkCharacterAnimationPrompt
|
||
* - buildImageSequencePrompt
|
||
*
|
||
* 当前仓库状态需要特别区分:
|
||
* - 当前自定义世界角色资产工坊默认输入框,实际直接使用前端
|
||
* src/prompts/customWorldRolePromptDefaults.ts
|
||
* - 默认描述文本的唯一主源已经统一为前端本地映射,
|
||
* 不再保留后端独立 bundle 编译接口
|
||
* - 当前正式角色主图与动作生成,仍然走本文件里的正式 prompt builder
|
||
*/
|
||
function clampPromptSeedText(value: unknown, maxLength: number) {
|
||
if (typeof value !== 'string') {
|
||
return '';
|
||
}
|
||
|
||
return value.replace(/\s+/gu, ' ').trim().slice(0, maxLength);
|
||
}
|
||
|
||
function sanitizeAnimationPromptText(value: string, maxLength: number) {
|
||
return value
|
||
.replace(/\s+/gu, ' ')
|
||
.replace(/血浆|喷血|鲜血|断肢|斩首|裸体|裸露|色情|性交/gu, '')
|
||
.replace(/死亡|死去|击杀/gu, '倒地结束')
|
||
.replace(/受击|受伤/gu, '失衡')
|
||
.replace(/砍杀|斩击/gu, '挥击')
|
||
.trim()
|
||
.slice(0, maxLength);
|
||
}
|
||
|
||
function buildCompactAnimationCharacterBrief(value: string) {
|
||
const normalized = sanitizeAnimationPromptText(value, 160);
|
||
if (!normalized) {
|
||
return '';
|
||
}
|
||
|
||
return normalized
|
||
.split(/[/|\n,,。;;]+/u)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean)
|
||
.slice(0, 4)
|
||
.join(',');
|
||
}
|
||
|
||
|
||
/**
|
||
* 正式角色主图 prompt 编译入口。
|
||
*
|
||
* 输入的 promptText 通常是资产工坊输入框里的“形象描述”文本;
|
||
* 这里会把它和角色摘要合并,再交给共享层 buildMasterPrompt,
|
||
* 产出真正发给图像模型的正式 prompt。
|
||
*
|
||
* 因此:
|
||
* - promptText = 默认描述文本层
|
||
* - buildNpcVisualPrompt 返回值 = 正式图像生成 prompt 层
|
||
*/
|
||
export function buildNpcVisualPrompt(promptText: string, characterBriefText = '') {
|
||
const mergedBrief = [characterBriefText.trim(), promptText.trim()]
|
||
.filter(Boolean)
|
||
.join('\n');
|
||
|
||
return buildMasterPrompt(
|
||
mergedBrief || '自定义世界角色,服装完整,姿态自然。',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 正式角色主图生成的负向提示词。
|
||
*
|
||
* 只服务于图像生成请求,不参与默认描述文本生成。
|
||
*/
|
||
export function buildNpcVisualNegativePrompt() {
|
||
return [
|
||
'正面视角',
|
||
'左朝向',
|
||
'完全 90 度纯右视图',
|
||
'镜头透视',
|
||
'半身像',
|
||
'脚被裁切',
|
||
'头顶被裁切',
|
||
'多角色',
|
||
'复杂背景',
|
||
'建筑场景',
|
||
'漂浮物',
|
||
'烟雾环境',
|
||
'武器消失',
|
||
'武器换手',
|
||
'额外手臂',
|
||
'额外腿',
|
||
'服装变化',
|
||
'脸部变化',
|
||
'模糊',
|
||
'运动模糊',
|
||
'文字',
|
||
'水印',
|
||
'UI 元素',
|
||
'软萌 Q版大头贴',
|
||
'儿童绘本风',
|
||
'厚涂插画感',
|
||
'低对比柔边',
|
||
].join(',');
|
||
}
|
||
|
||
/**
|
||
* 连续序列帧方案的正式动作 prompt。
|
||
*
|
||
* 这是“图像序列帧”动作生成链路使用的正式 prompt,
|
||
* 不属于默认描述文本层。
|
||
*/
|
||
export function buildImageSequencePrompt(
|
||
animation: string,
|
||
promptText: string,
|
||
frameCount: number,
|
||
useChromaKey: boolean,
|
||
) {
|
||
return [
|
||
`同一角色连续 ${frameCount} 帧动作序列,动作主题是 ${animation}。`,
|
||
'固定机位,单人,全身,侧身朝右,保持同一套服装、发型、武器和体型。',
|
||
'帧间动作连续,姿态逐步推进,不要换人,不要跳变,不要多余物体。',
|
||
useChromaKey
|
||
? '纯绿色背景,无地面装饰,方便后期抠像。'
|
||
: '背景尽量纯净,避免复杂场景。',
|
||
promptText.trim(),
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ');
|
||
}
|
||
|
||
/**
|
||
* 通用动作视频方案的正式动作 prompt。
|
||
*
|
||
* 输入的 promptText 是动作描述文本;
|
||
* 输出的是可以直接提交给动作模型的视频 prompt。
|
||
*
|
||
* 当前仓库里它主要服务于非 Ark 的动作视频链路,
|
||
* 以及某些保留的动作生成策略。
|
||
*/
|
||
export function buildNpcAnimationPrompt(options: {
|
||
animation: string;
|
||
promptText: string;
|
||
useChromaKey: boolean;
|
||
loop: boolean;
|
||
characterBriefText?: string;
|
||
actionTemplateId?: string;
|
||
}) {
|
||
const characterBrief = buildCompactAnimationCharacterBrief(
|
||
options.characterBriefText ?? '',
|
||
);
|
||
const actionDetailText = sanitizeAnimationPromptText(options.promptText, 140);
|
||
const loopRule = options.loop
|
||
? '这是循环动作,直接进入动作循环中段,不要开场静止站桩,不要把主参考图原样作为第一帧。'
|
||
: options.animation === 'die'
|
||
? '这是死亡终结动作,首帧参考主图角色形象即可,尾帧停在死亡结束姿态,不要回到主图形象。'
|
||
: '这是非循环动作,首帧和尾帧都要回到参考主图角色形象,中段完成动作变化。';
|
||
|
||
if (options.actionTemplateId) {
|
||
return [
|
||
buildVideoActionPrompt({
|
||
actionTemplate: getActionTemplateById(
|
||
options.actionTemplateId as Parameters<
|
||
typeof getActionTemplateById
|
||
>[0],
|
||
),
|
||
actionDetailText,
|
||
useChromaKey: options.useChromaKey,
|
||
characterBrief: characterBrief || `${options.animation} 动作角色`,
|
||
}),
|
||
loopRule,
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ');
|
||
}
|
||
|
||
return [
|
||
`单人 NPC 全身动作视频,动作主题是 ${options.animation}。`,
|
||
'角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。',
|
||
'动作连贯,避免服装、发型、面部、武器随机漂移。',
|
||
options.useChromaKey
|
||
? '背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。'
|
||
: '背景简洁纯净,无复杂场景。',
|
||
characterBrief ? `角色设定:${characterBrief}` : '',
|
||
actionDetailText,
|
||
loopRule,
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ');
|
||
}
|
||
|
||
/**
|
||
* Ark 图生视频动作链路的正式动作 prompt。
|
||
*
|
||
* 当前自定义世界角色资产工坊的主动作生成流程,
|
||
* 最终会走到这个 builder。它会在共享模板的基础上,
|
||
* 叠加 Ark 所需的首帧 / 尾帧约束与动作英文名约束。
|
||
*/
|
||
export function buildArkCharacterAnimationPrompt(options: {
|
||
animation: string;
|
||
promptText: string;
|
||
useChromaKey: boolean;
|
||
loop: boolean;
|
||
characterBriefText?: string;
|
||
actionTemplateId?: string;
|
||
}) {
|
||
const normalizedAnimationName =
|
||
options.animation.trim().replace(/\s+/gu, '_') || 'idle';
|
||
const characterBrief = buildCompactAnimationCharacterBrief(
|
||
options.characterBriefText ?? '',
|
||
);
|
||
const actionDetailText = sanitizeAnimationPromptText(options.promptText, 140);
|
||
const frameRule = options.loop
|
||
? '首帧严格使用图片1,尾帧严格使用图片2,循环动作必须自然闭环,不要静止开场。'
|
||
: '首帧严格使用图片1,尾帧严格使用图片2,中段完成完整动作变化,收束干净。';
|
||
|
||
if (options.actionTemplateId) {
|
||
return [
|
||
buildVideoActionPrompt({
|
||
actionTemplate: getActionTemplateById(
|
||
options.actionTemplateId as Parameters<typeof getActionTemplateById>[0],
|
||
),
|
||
actionDetailText,
|
||
useChromaKey: options.useChromaKey,
|
||
characterBrief: characterBrief || `${normalizedAnimationName} action role`,
|
||
}),
|
||
`动作英文名:${normalizedAnimationName}。`,
|
||
frameRule,
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ');
|
||
}
|
||
|
||
return [
|
||
`单人 NPC 全身动作视频,动作英文名是 ${normalizedAnimationName}。`,
|
||
'角色固定为图片1和图片2中的同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。',
|
||
'动作连贯,避免服装、发型、面部、武器随机漂移,不要多角色,不要镜头切换。',
|
||
options.useChromaKey
|
||
? '背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。'
|
||
: '背景简洁纯净,无复杂场景。',
|
||
characterBrief ? `角色设定:${characterBrief}` : '',
|
||
actionDetailText ? `动作细节:${actionDetailText}` : '',
|
||
frameRule,
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ');
|
||
}
|
||
|
||
/**
|
||
* 当正式动作 prompt 触发审核或兼容问题时的保守兜底动作 prompt。
|
||
*
|
||
* 这条链路是正式动作生成的降级方案,不参与默认描述文本生成。
|
||
*/
|
||
export function buildFallbackModerationSafeAnimationPrompt(options: {
|
||
animation: string;
|
||
loop: boolean;
|
||
useChromaKey: boolean;
|
||
}) {
|
||
return [
|
||
`单人全身角色动作视频,动作主题是 ${options.animation}。`,
|
||
'角色固定为同一人,右向斜侧身,镜头稳定,轮廓清楚。',
|
||
options.loop
|
||
? '循环动作直接进入稳定循环,不要静止开场,不要定格首帧。'
|
||
: '非循环动作首尾回到角色标准站姿,中段完成动作变化。',
|
||
options.useChromaKey
|
||
? '背景为纯绿色绿幕,无其他人物和场景元素。'
|
||
: '背景简洁纯净。',
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ');
|
||
}
|