@@ -5,7 +5,7 @@ import {
|
||||
} from '../../types';
|
||||
|
||||
export const MASTER_VISUAL_WIDTH = 1024;
|
||||
export const MASTER_VISUAL_HEIGHT = 1536;
|
||||
export const MASTER_VISUAL_HEIGHT = 1024;
|
||||
export const GENERATED_FRAME_WIDTH = 192;
|
||||
export const GENERATED_FRAME_HEIGHT = 256;
|
||||
|
||||
@@ -769,6 +769,34 @@ async function normalizeFrameSourceToDataUrl(
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
export async function normalizeMasterVisualSourceToDataUrl(
|
||||
source: string,
|
||||
options: {
|
||||
applyChromaKey?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const image = await loadImageFromSource(source);
|
||||
const { canvas, context } = createCanvas(
|
||||
MASTER_VISUAL_WIDTH,
|
||||
MASTER_VISUAL_HEIGHT,
|
||||
);
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
drawContainedImage(context, image, {
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
});
|
||||
|
||||
if (options.applyChromaKey !== false) {
|
||||
applyGreenScreenAlpha(context, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
return {
|
||||
dataUrl: canvas.toDataURL('image/png'),
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
};
|
||||
}
|
||||
|
||||
function seekVideo(video: HTMLVideoElement, targetTime: number) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (Math.abs(video.currentTime - targetTime) < 0.001) {
|
||||
|
||||
@@ -6,6 +6,10 @@ import { fetchJson } from '../../editor/shared/jsonClient';
|
||||
|
||||
export const CHARACTER_VISUAL_GENERATE_API_PATH =
|
||||
ASSET_API_PATHS.characterVisualGenerate;
|
||||
export const CHARACTER_PROMPT_BUNDLE_GENERATE_API_PATH =
|
||||
ASSET_API_PATHS.characterPromptBundleGenerate;
|
||||
export const CHARACTER_WORKFLOW_CACHE_API_PATH =
|
||||
ASSET_API_PATHS.characterWorkflowCache;
|
||||
export const CHARACTER_VISUAL_PUBLISH_API_PATH =
|
||||
ASSET_API_PATHS.characterVisualPublish;
|
||||
export const CHARACTER_VISUAL_JOB_API_PATH = ASSET_API_PATHS.characterVisualJobs;
|
||||
@@ -43,6 +47,43 @@ export type CharacterVisualDraft = {
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type CharacterPromptBundlePayload = {
|
||||
roleKind: 'playable' | 'story';
|
||||
characterName: string;
|
||||
roleTitle?: string;
|
||||
roleLabel?: string;
|
||||
description?: string;
|
||||
backstory?: string;
|
||||
personality?: string;
|
||||
motivation?: string;
|
||||
combatStyle?: string;
|
||||
tags?: string[];
|
||||
characterBriefText: string;
|
||||
};
|
||||
|
||||
export type CharacterPromptBundleResult = {
|
||||
ok: true;
|
||||
visualPromptText: string;
|
||||
animationPromptText: string;
|
||||
scenePromptText: string;
|
||||
source: 'llm' | 'fallback';
|
||||
model: string | null;
|
||||
};
|
||||
|
||||
export type CharacterAssetWorkflowCache = {
|
||||
characterId: string;
|
||||
visualPromptText: string;
|
||||
animationPromptText: string;
|
||||
visualDrafts: CharacterVisualDraft[];
|
||||
selectedVisualDraftId: string;
|
||||
selectedAnimation: string;
|
||||
imageSrc?: string;
|
||||
generatedVisualAssetId?: string;
|
||||
generatedAnimationSetId?: string;
|
||||
animationMap?: Record<string, unknown> | null;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type CharacterVisualGenerationPayload = {
|
||||
characterId: string;
|
||||
sourceMode: Exclude<CharacterVisualSourceMode, 'upload'>;
|
||||
@@ -129,7 +170,41 @@ export async function generateCharacterVisualCandidates(
|
||||
model: string;
|
||||
prompt: string;
|
||||
drafts: CharacterVisualDraft[];
|
||||
}>(CHARACTER_VISUAL_GENERATE_API_PATH, payload, '生成角色主形象候选失败');
|
||||
}>(CHARACTER_VISUAL_GENERATE_API_PATH, payload, '生成角色主形象失败');
|
||||
}
|
||||
|
||||
export async function generateCharacterPromptBundle(
|
||||
payload: CharacterPromptBundlePayload,
|
||||
) {
|
||||
return postApiJson<CharacterPromptBundleResult>(
|
||||
CHARACTER_PROMPT_BUNDLE_GENERATE_API_PATH,
|
||||
payload,
|
||||
'生成默认提示词失败',
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchCharacterWorkflowCache(characterId: string) {
|
||||
return fetchJson<{
|
||||
ok: true;
|
||||
cache: CharacterAssetWorkflowCache | null;
|
||||
}>(
|
||||
`${CHARACTER_WORKFLOW_CACHE_API_PATH}/${encodeURIComponent(characterId)}`,
|
||||
'读取角色形象生成缓存失败',
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveCharacterWorkflowCache(
|
||||
payload: CharacterAssetWorkflowCache,
|
||||
) {
|
||||
return postApiJson<{
|
||||
ok: true;
|
||||
cache: CharacterAssetWorkflowCache;
|
||||
saveMessage: string;
|
||||
}>(
|
||||
CHARACTER_WORKFLOW_CACHE_API_PATH,
|
||||
payload,
|
||||
'保存角色形象生成缓存失败',
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchCharacterVisualJobStatus(taskId: string) {
|
||||
|
||||
76
src/components/asset-studio/customWorldRolePromptDefaults.ts
Normal file
76
src/components/asset-studio/customWorldRolePromptDefaults.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export type PromptDefaultRole = {
|
||||
name: string;
|
||||
title: string;
|
||||
role: string;
|
||||
description?: string;
|
||||
backstory?: string;
|
||||
personality?: string;
|
||||
motivation?: string;
|
||||
combatStyle?: string;
|
||||
tags?: string[];
|
||||
};
|
||||
|
||||
export type CustomWorldRolePromptBundle = {
|
||||
visualPromptText: string;
|
||||
animationPromptText: string;
|
||||
scenePromptText: string;
|
||||
};
|
||||
|
||||
function cleanSeedText(value: string | undefined, maxLength: number) {
|
||||
return (value ?? '').replace(/\s+/gu, ' ').trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
export function buildDefaultRolePromptBundle(
|
||||
role: PromptDefaultRole,
|
||||
): CustomWorldRolePromptBundle {
|
||||
const characterName = cleanSeedText(role.name, 40) || '该角色';
|
||||
const roleAnchor =
|
||||
[cleanSeedText(role.title, 60), cleanSeedText(role.role, 60)]
|
||||
.filter(Boolean)
|
||||
.join(' / ') || '关键角色';
|
||||
const descriptionAnchor =
|
||||
cleanSeedText(role.description, 220) ||
|
||||
cleanSeedText(role.backstory, 260) ||
|
||||
cleanSeedText(role.personality, 160) ||
|
||||
'识别度鲜明';
|
||||
const combatAnchor =
|
||||
cleanSeedText(role.combatStyle, 180) ||
|
||||
cleanSeedText(role.motivation, 180) ||
|
||||
'动作重心稳定';
|
||||
const tagAnchor =
|
||||
role.tags && role.tags.length > 0
|
||||
? `保留 ${role.tags.slice(0, 8).join('、')} 的角色识别点。`
|
||||
: '';
|
||||
|
||||
return {
|
||||
visualPromptText: [
|
||||
`${characterName},${roleAnchor}。`,
|
||||
'单人全身,2D 横版 RPG 角色主图,侧身朝右,脚底完整可见,服装、发型、武器与轮廓保持稳定清楚。',
|
||||
`外观气质围绕:${descriptionAnchor}。`,
|
||||
`动作识别点参考:${combatAnchor}。`,
|
||||
tagAnchor,
|
||||
'构图干净,主体明确,不做正面立绘,不做夸张透视。',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
animationPromptText: [
|
||||
`${characterName}核心动作试片。`,
|
||||
'保持同一角色的服装、发型、武器和体型一致,镜头稳定,侧身朝右,动作连贯自然。',
|
||||
`动作气质参考:${combatAnchor}。`,
|
||||
role.personality ? `角色状态补充:${cleanSeedText(role.personality, 160)}。` : '',
|
||||
'起手清楚,发力明确,收招干净,避免漂移、乱摆和形体变形。',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
scenePromptText: [
|
||||
`${characterName}关联主场景,适合作为首次登场区域或常驻活动空间。`,
|
||||
'16:9 横版 RPG 场景背景,上半部分突出中远景氛围,下半部分是清晰可站立地面。',
|
||||
`场景叙事气质围绕:${descriptionAnchor}。`,
|
||||
role.backstory ? `环境背景可埋入:${cleanSeedText(role.backstory, 260)}。` : '',
|
||||
role.motivation ? `场景目标暗示可参考:${cleanSeedText(role.motivation, 160)}。` : '',
|
||||
'整体风格统一克制,适合作为剧情探索与战斗底图。',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user