Files
Genarrative/src/components/asset-studio/characterAssetWorkflowPersistence.ts
T
2026-05-28 15:42:46 +08:00

377 lines
10 KiB
TypeScript

import {
ASSET_API_PATHS,
postApiJson,
} from '../../editor/shared/editorApiClient';
import {
appendApiErrorRequestId,
fetchJson,
parseApiErrorMessage,
} from '../../editor/shared/jsonClient';
export const CHARACTER_VISUAL_GENERATE_API_PATH =
ASSET_API_PATHS.characterVisualGenerate;
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;
export const CHARACTER_ANIMATION_GENERATE_API_PATH =
ASSET_API_PATHS.characterAnimationGenerate;
export const CHARACTER_ANIMATION_PUBLISH_API_PATH =
ASSET_API_PATHS.characterAnimationPublish;
export const CHARACTER_ANIMATION_JOB_API_PATH =
ASSET_API_PATHS.characterAnimationJobs;
export const CHARACTER_ANIMATION_IMPORT_VIDEO_API_PATH =
ASSET_API_PATHS.characterAnimationImportVideo;
export const CHARACTER_ANIMATION_TEMPLATES_API_PATH =
ASSET_API_PATHS.characterAnimationTemplates;
export const ROLE_ASSET_WORKFLOW_API_PATH =
'/api/runtime/custom-world/asset-studio/role';
export type CharacterVisualSourceMode =
| 'text-to-image'
| 'image-to-image'
| 'upload';
export type CharacterAnimationStrategy =
| 'image-sequence'
| 'image-to-video'
| 'motion-transfer'
| 'reference-to-video';
export type CharacterMotionTransferModel =
| 'wan2.2-animate-move'
| 'wan2.2-animate-mix';
export type CharacterVisualDraft = {
id: string;
label: string;
imageSrc: string;
width: number;
height: number;
};
export type CharacterAssetWorkflowCache = {
characterId: string;
cacheScopeId?: string;
visualPromptText: string;
animationPromptText: string;
animationPromptTextByKey?: Record<string, string>;
visualDrafts: CharacterVisualDraft[];
selectedVisualDraftId: string;
selectedAnimation: string;
imageSrc?: string;
generatedVisualAssetId?: string;
generatedAnimationSetId?: string;
animationMap?: Record<string, unknown> | null;
updatedAt?: string;
};
export type CharacterAssetRolePromptInput = {
id: string;
name?: string;
title?: string;
role?: string;
visualDescription?: string;
actionDescription?: string;
sceneVisualDescription?: string;
description?: string;
backstory?: string;
personality?: string;
motivation?: string;
combatStyle?: string;
tags?: string[];
imageSrc?: string;
generatedVisualAssetId?: string;
generatedAnimationSetId?: string;
animationMap?: Record<string, unknown> | null;
};
export type CharacterRolePromptBundle = {
visualPromptText: string;
animationPromptText: string;
scenePromptText: string;
};
export type CharacterRoleAssetWorkflow = {
role: CharacterAssetRolePromptInput;
defaultPromptBundle: CharacterRolePromptBundle;
visualPromptText: string;
animationPromptText: string;
animationPromptTextByKey: Record<string, 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'>;
promptText: string;
referenceImageDataUrls: string[];
candidateCount: number;
imageModel: string;
size: string;
};
export type CharacterVisualPublishPayload = {
characterId: string;
sourceMode: CharacterVisualSourceMode;
promptText: string;
selectedPreviewSource: string;
previewSources: string[];
width: number;
height: number;
updateCharacterOverride?: boolean;
};
export type CharacterAnimationGenerationPayload = {
characterId: string;
strategy: CharacterAnimationStrategy;
animation: string;
promptText: string;
characterBriefText?: string;
actionTemplateId?: string;
visualSource: string;
referenceImageDataUrls: string[];
referenceVideoDataUrls: string[];
lastFrameImageDataUrl?: string;
frameCount: number;
fps: number;
durationSeconds: number;
loop: boolean;
useChromaKey: boolean;
resolution: string;
ratio: string;
imageSequenceModel: string;
videoModel: string;
referenceVideoModel: string;
motionTransferModel: CharacterMotionTransferModel;
};
export type CharacterAnimationDraftPayload = {
framesDataUrls: string[];
fps: number;
loop: boolean;
frameWidth: number;
frameHeight: number;
frameCount?: number;
applyChromaKey?: boolean;
sampleStartRatio?: number;
sampleEndRatio?: number;
previewVideoPath?: string;
};
export type CharacterAnimationTemplate = {
id: string;
label: string;
animation: string;
promptSuffix: string;
notes: string;
};
export type CharacterAssetJobStatus = {
taskId: string;
kind: 'visual' | 'animation';
status: 'queued' | 'running' | 'completed' | 'failed';
characterId: string;
animation?: string;
strategy?: CharacterAnimationStrategy;
model: string;
prompt: string;
createdAt: string;
updatedAt: string;
result?: Record<string, unknown>;
errorMessage?: string;
};
export async function generateCharacterVisualCandidates(
payload: CharacterVisualGenerationPayload,
) {
return postApiJson<{
ok: true;
taskId: string;
model: string;
prompt: string;
drafts: CharacterVisualDraft[];
}>(CHARACTER_VISUAL_GENERATE_API_PATH, payload, '生成角色主形象失败');
}
export async function fetchCharacterWorkflowCache(
characterId: string,
cacheScopeId?: string,
) {
return fetchJson<{
ok: true;
cache: CharacterAssetWorkflowCache | null;
}>(
`${CHARACTER_WORKFLOW_CACHE_API_PATH}/${encodeURIComponent(characterId)}${
cacheScopeId
? `?cacheScopeId=${encodeURIComponent(cacheScopeId)}`
: ''
}`,
'读取角色形象生成缓存失败',
);
}
export async function saveCharacterWorkflowCache(
payload: CharacterAssetWorkflowCache,
) {
return postApiJson<{
ok: true;
cache: CharacterAssetWorkflowCache;
saveMessage: string;
}>(
CHARACTER_WORKFLOW_CACHE_API_PATH,
payload,
'保存角色形象生成缓存失败',
);
}
export async function resolveCharacterRoleAssetWorkflow(payload: {
characterId: string;
cacheScopeId?: string;
role: CharacterAssetRolePromptInput;
}) {
const { characterId, cacheScopeId, role } = payload;
return postApiJson<{
ok: true;
cache: CharacterAssetWorkflowCache | null;
workflow: CharacterRoleAssetWorkflow;
}>(
`${ROLE_ASSET_WORKFLOW_API_PATH}/${encodeURIComponent(characterId)}/workflow`,
{
cacheScopeId,
role,
},
'读取角色资产工坊工作流失败',
);
}
export async function putCharacterRoleAssetWorkflow(
payload: CharacterAssetWorkflowCache,
) {
const url = `${ROLE_ASSET_WORKFLOW_API_PATH}/${encodeURIComponent(payload.characterId)}/workflow`;
const response = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const responseText = await response.text();
if (!response.ok) {
throw new Error(
appendApiErrorRequestId(
parseApiErrorMessage(responseText, '保存角色资产工坊缓存失败'),
response.headers.get('x-request-id'),
),
);
}
return responseText
? (JSON.parse(responseText) as {
ok: true;
cache: CharacterAssetWorkflowCache;
saveMessage: string;
})
: ({
ok: true,
cache: payload,
saveMessage: '',
} as {
ok: true;
cache: CharacterAssetWorkflowCache;
saveMessage: string;
});
}
export async function fetchCharacterVisualJobStatus(taskId: string) {
return fetchJson<CharacterAssetJobStatus>(
`${CHARACTER_VISUAL_JOB_API_PATH}/${encodeURIComponent(taskId)}`,
'读取角色主形象任务状态失败',
);
}
export async function publishCharacterVisualAsset(
payload: CharacterVisualPublishPayload,
) {
return postApiJson<{
ok: true;
assetId: string;
portraitPath: string;
overrideMap: Record<string, unknown>;
saveMessage: string;
}>(CHARACTER_VISUAL_PUBLISH_API_PATH, payload, '发布角色主形象失败');
}
export async function generateCharacterAnimationDraft(
payload: CharacterAnimationGenerationPayload,
) {
return postApiJson<
| {
ok: true;
taskId: string;
strategy: 'image-sequence';
model: string;
prompt: string;
imageSources: string[];
}
| {
ok: true;
taskId: string;
strategy: 'image-to-video' | 'motion-transfer' | 'reference-to-video';
model: string;
prompt: string;
previewVideoPath: string;
}
>(CHARACTER_ANIMATION_GENERATE_API_PATH, payload, '生成角色动作草稿失败');
}
export async function fetchCharacterAnimationJobStatus(taskId: string) {
return fetchJson<CharacterAssetJobStatus>(
`${CHARACTER_ANIMATION_JOB_API_PATH}/${encodeURIComponent(taskId)}`,
'读取角色动作任务状态失败',
);
}
export async function fetchCharacterAnimationTemplates() {
return fetchJson<{
ok: true;
templates: CharacterAnimationTemplate[];
}>(CHARACTER_ANIMATION_TEMPLATES_API_PATH, '读取动作模板列表失败');
}
export async function importCharacterAnimationVideo(payload: {
characterId: string;
animation: string;
videoSource: string;
sourceLabel?: string;
}) {
return postApiJson<{
ok: true;
importedVideoPath: string;
draftId: string;
saveMessage: string;
}>(CHARACTER_ANIMATION_IMPORT_VIDEO_API_PATH, payload, '导入动作视频失败');
}
export async function publishCharacterAnimationAssets(payload: {
characterId: string;
visualAssetId: string;
animations: Record<string, CharacterAnimationDraftPayload>;
updateCharacterOverride?: boolean;
}) {
return postApiJson<{
ok: true;
animationSetId: string;
overrideMap: Record<string, unknown>;
animationMap: Record<string, unknown>;
saveMessage: string;
}>(CHARACTER_ANIMATION_PUBLISH_API_PATH, payload, '发布角色基础动作失败');
}