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

This commit is contained in:
2026-04-16 21:47:20 +08:00
parent 2456c10c63
commit 09d4c0c31b
79 changed files with 11873 additions and 2341 deletions

View File

@@ -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) {

View File

@@ -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) {

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