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[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(' '); }