Files
Genarrative/src/services/image-editor/editorProjectClient.ts
T
JenkenB ea29887d9c 完善快速编辑参考图与生成占位
- 快速编辑支持最多8张额外参考图,并将原图自动作为最后一张参考图提交

- 快速编辑尺寸和模型沿用原图配置,按禁用态参数胶囊展示

- 快速编辑提交前同步改写原图引用为实际图序号

- 快速编辑生成改为创建独立画布生成占位,并支持键盘删除生成中对象

- 对齐生成类面板参考图图标、规范类固定参数和相关文档测试
2026-06-18 22:50:42 +08:00

687 lines
18 KiB
TypeScript

import { requestJson } from '../apiClient';
const EDITOR_PROJECT_API_BASE = '/api/editor/projects';
const EDITOR_ASSET_API_BASE = '/api/editor/assets';
const EDITOR_IMAGE_GENERATION_API = '/api/editor/images/generations';
const EDITOR_IMAGE_EDIT_API = '/api/editor/images/edits';
const EDITOR_ICON_SPRITESHEET_GENERATION_API =
'/api/editor/icon-spritesheets/generations';
const EDITOR_CHARACTER_ANIMATION_GENERATION_API =
'/api/editor/character-animations/generations';
const EDITOR_VIDEO_GENERATION_API = '/api/editor/videos/generations';
const EDITOR_SOUND_EFFECT_GENERATION_API =
'/api/editor/audios/sound-effects/generations';
const EDITOR_BACKGROUND_MUSIC_GENERATION_API =
'/api/editor/audios/background-music/generations';
const EDITOR_IMAGE_MODEL_NANOBANANA2 = 'gemini-3.1-flash-image-preview';
const DEFAULT_PROJECT_TITLE = '未命名画布';
export type EditorCanvasViewport = {
x: number;
y: number;
scale: number;
};
export type EditorProjectLayerSnapshot = Record<string, unknown> & {
layerId: string;
resourceId: string;
};
export type EditorProjectResourceSourceType =
| 'uploaded'
| 'generated'
| 'mock_generated';
export type EditorProjectResourceSnapshot = {
resourceId: string;
projectId: string;
imageSrc: string;
objectKey?: string | null;
assetObjectId?: string | null;
width: number;
height: number;
sourceType: EditorProjectResourceSourceType;
prompt?: string | null;
actualPrompt?: string | null;
model?: string | null;
provider?: string | null;
taskId?: string | null;
durationSeconds?: number | null;
sourceResourceId?: string | null;
createdAt?: string;
updatedAt?: string;
};
export type EditorAssetFolderSnapshot = {
folderId: string;
label: string;
sortOrder: number;
collapsed: boolean;
systemDefault: boolean;
createdAt?: string;
updatedAt?: string;
};
export type EditorAssetSnapshot = {
assetId: string;
folderId: string;
label: string;
imageSrc: string;
objectKey?: string | null;
assetObjectId?: string | null;
width: number;
height: number;
sourceType: EditorProjectResourceSourceType;
prompt?: string | null;
actualPrompt?: string | null;
model?: string | null;
provider?: string | null;
taskId?: string | null;
durationSeconds?: number | null;
createdAt?: string;
updatedAt?: string;
};
export type EditorAssetLibrarySnapshot = {
folders: EditorAssetFolderSnapshot[];
assets: EditorAssetSnapshot[];
};
export type EditorImageGenerationInput = {
prompt: string;
size?: string;
kind?: 'spec' | 'character' | 'quick-edit' | 'ui-design';
model?: string;
aspectRatio?: string;
imageSize?: string;
referenceImageSrcs?: string[];
};
export type EditorIconSpritesheetGenerationInput = {
referenceImageSrc: string;
iconDescriptions: string[];
model?: string;
aspectRatio?: string;
imageSize?: string;
priceMudPoints: number;
};
export type EditorImageEditInput = {
prompt: string;
sourceImageSrc: string;
size?: string;
model?: string;
};
export type EditorImageGenerationResult = {
imageSrc: string;
objectKey?: string | null;
assetObjectId?: string | null;
width: number;
height: number;
sourceType: 'generated';
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
};
export type EditorIconSpritesheetIconResult = {
name: string;
imageSrc: string;
width: number;
height: number;
};
export type EditorIconSpritesheetGenerationResult = {
spritesheetImageSrc: string;
spritesheetWidth: number;
spritesheetHeight: number;
iconImageSrcs: EditorIconSpritesheetIconResult[];
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
priceMudPoints: number;
};
export type EditorCharacterAnimationResolution = '480p' | '720p';
export type EditorCharacterAnimationRatio =
| 'same'
| '1:1'
| '4:3'
| '16:9'
| '9:16'
| '3:4';
export type EditorCharacterAnimationFrameCount = 32 | 40 | 48;
export type EditorCharacterAnimationDurationSeconds = 4 | 5 | 6;
export type EditorCharacterAnimationGenerationInput = {
sourceLayerId: string;
sourceImageSrc: string;
sourceWidth: number;
sourceHeight: number;
promptText: string;
resolution: EditorCharacterAnimationResolution;
ratio: EditorCharacterAnimationRatio;
frameCount: EditorCharacterAnimationFrameCount;
durationSeconds: EditorCharacterAnimationDurationSeconds;
priceMudPoints: number;
model: 'seedance2.0-fast';
};
export type EditorCharacterAnimationFrameResult = {
frameIndex: number;
imageSrc: string;
width: number;
height: number;
};
export type EditorCharacterAnimationGenerationResult = {
taskId: string;
model: 'seedance2.0-fast';
prompt: string;
previewVideoPath: string;
frames: EditorCharacterAnimationFrameResult[];
frameCount: number;
durationSeconds: number;
fps: number;
priceMudPoints: number;
};
export type EditorVideoModel =
| 'seedance2.0'
| 'seedance2.0-fast'
| 'kling3.0'
| 'kling3.0-omni'
| 'veo3.1'
| 'veo3.1-fast';
export type EditorVideoGenerationInput = {
prompt: string;
model: EditorVideoModel;
aspectRatio: '16:9';
durationSeconds: 4 | 5;
resolution: '480p' | '720p';
mode: 'std';
sound: 'off';
priceMudPoints: number;
referenceImageSrcs?: string[];
referenceVideoSrcs?: string[];
referenceAudioSrcs?: string[];
};
export type EditorVideoGenerationResult = {
videoSrc: string;
width: number;
height: number;
sourceType: 'generated';
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
priceMudPoints?: number;
};
export type EditorSoundEffectGenerationInput = {
sound: string;
type: 'one-shot' | 'loop';
tempo: number | null;
priceMudPoints: number;
};
export type EditorBackgroundMusicGenerationInput = {
gptDescriptionPrompt: string;
makeInstrumental: true;
priceMudPoints: number;
};
export type EditorAudioGenerationResult = {
audioSrc: string;
width: number;
height: number;
sourceType: 'generated';
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
priceMudPoints: number;
audioKind: 'sound-effect' | 'background-music';
durationSeconds?: number | null;
};
export type EditorProjectSnapshot = {
projectId: string;
title: string;
canvas?: EditorCanvasSnapshot;
viewport: EditorCanvasViewport;
layers: EditorProjectLayerSnapshot[];
resources: EditorProjectResourceSnapshot[];
updatedAt: string;
};
export type EditorCanvasSnapshot = {
canvasId: string;
projectId: string;
title: string;
viewport: EditorCanvasViewport;
layers: EditorProjectLayerSnapshot[];
createdAt?: string;
updatedAt: string;
};
export type EditorProjectCreateInput = {
title?: string;
};
export type EditorProjectLayoutSaveInput = {
viewport: EditorCanvasViewport;
layers: EditorProjectLayerSnapshot[];
};
export type EditorProjectResourceCreateInput = {
imageSrc: string;
objectKey?: string | null;
assetObjectId?: string | null;
width: number;
height: number;
sourceType: EditorProjectResourceSourceType;
prompt?: string | null;
actualPrompt?: string | null;
model?: string | null;
provider?: string | null;
taskId?: string | null;
durationSeconds?: number | null;
sourceResourceId?: string | null;
};
export type EditorAssetCreateInput = {
folderId: string;
label: string;
imageSrc: string;
objectKey?: string | null;
assetObjectId?: string | null;
width: number;
height: number;
sourceType: EditorProjectResourceSourceType;
prompt?: string | null;
actualPrompt?: string | null;
model?: string | null;
provider?: string | null;
taskId?: string | null;
durationSeconds?: number | null;
};
export type EditorAssetUpdateInput = {
label?: string;
folderId?: string;
};
type EditorProjectResponse = {
project: EditorProjectSnapshot;
};
type EditorProjectRecentResponse = {
project: EditorProjectSnapshot | null;
};
type EditorProjectResourceResponse = {
resource: EditorProjectResourceSnapshot;
};
type EditorAssetLibraryResponse = {
library: EditorAssetLibrarySnapshot;
};
type EditorAssetFolderResponse = {
folder: EditorAssetFolderSnapshot;
};
type EditorAssetFolderDeleteResponse = {
library: EditorAssetLibrarySnapshot;
};
type EditorAssetResponse = {
asset: EditorAssetSnapshot;
};
type EditorImageGenerationResponse = EditorImageGenerationResult;
type EditorIconSpritesheetGenerationResponse =
EditorIconSpritesheetGenerationResult;
type EditorVideoGenerationResponse = EditorVideoGenerationResult;
type EditorAudioGenerationResponse = EditorAudioGenerationResult;
function jsonRequest(method: 'POST' | 'PATCH', body: Record<string, unknown>) {
return {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
};
}
export async function listEditorProjects() {
const response = await requestJson<{ projects: EditorProjectSnapshot[] }>(
EDITOR_PROJECT_API_BASE,
{ method: 'GET' },
'读取图片画布工程列表失败',
);
return response.projects;
}
export async function loadRecentEditorProject() {
return requestJson<EditorProjectRecentResponse>(
`${EDITOR_PROJECT_API_BASE}/recent`,
{ method: 'GET' },
'读取图片画布工程失败',
);
}
export async function createEditorProject(
input: EditorProjectCreateInput = {},
) {
const response = await requestJson<EditorProjectResponse>(
EDITOR_PROJECT_API_BASE,
jsonRequest('POST', {
title: input.title?.trim() || DEFAULT_PROJECT_TITLE,
}),
'创建图片画布工程失败',
);
return response.project;
}
export async function loadOrCreateRecentEditorProject() {
const response = await loadRecentEditorProject();
if (response.project) {
return response.project;
}
return createEditorProject({ title: DEFAULT_PROJECT_TITLE });
}
export async function loadEditorProject(projectId: string) {
const response = await requestJson<EditorProjectResponse>(
`${EDITOR_PROJECT_API_BASE}/${encodeURIComponent(projectId)}`,
{ method: 'GET' },
'读取图片画布工程失败',
);
return response.project;
}
export async function renameEditorProject(projectId: string, title: string) {
const response = await requestJson<EditorProjectResponse>(
`${EDITOR_PROJECT_API_BASE}/${encodeURIComponent(projectId)}/metadata`,
jsonRequest('PATCH', { title }),
'重命名图片画布工程失败',
);
return response.project;
}
export async function deleteEditorProject(projectId: string) {
const response = await requestJson<{ deletedProjectId: string }>(
`${EDITOR_PROJECT_API_BASE}/${encodeURIComponent(projectId)}`,
{ method: 'DELETE' },
'删除图片画布工程失败',
);
return response.deletedProjectId;
}
export async function saveEditorProjectLayout(
projectId: string,
input: EditorProjectLayoutSaveInput,
) {
const response = await requestJson<EditorProjectResponse>(
`${EDITOR_PROJECT_API_BASE}/${encodeURIComponent(projectId)}`,
jsonRequest('PATCH', {
viewport: input.viewport,
layers: input.layers,
}),
'保存图片画布工程失败',
);
return response.project;
}
export async function createEditorProjectResource(
projectId: string,
input: EditorProjectResourceCreateInput,
) {
const response = await requestJson<EditorProjectResourceResponse>(
`${EDITOR_PROJECT_API_BASE}/${encodeURIComponent(projectId)}/resources`,
jsonRequest('POST', { ...input }),
'创建图片画布资源失败',
);
return response.resource;
}
export async function loadEditorAssetLibrary() {
const response = await requestJson<EditorAssetLibraryResponse>(
`${EDITOR_ASSET_API_BASE}/library`,
{ method: 'GET' },
'读取图片画布素材库失败',
);
return response.library;
}
export async function createEditorAssetFolder(
label: string,
sortOrder?: number,
) {
const response = await requestJson<EditorAssetFolderResponse>(
`${EDITOR_ASSET_API_BASE}/folders`,
jsonRequest('POST', { label, sortOrder }),
'创建图片画布素材文件夹失败',
);
return response.folder;
}
export async function updateEditorAssetFolder(
folderId: string,
input: { label?: string; collapsed?: boolean },
) {
const response = await requestJson<EditorAssetFolderResponse>(
`${EDITOR_ASSET_API_BASE}/folders/${encodeURIComponent(folderId)}`,
jsonRequest('PATCH', input),
'更新图片画布素材文件夹失败',
);
return response.folder;
}
export async function deleteEditorAssetFolder(folderId: string) {
const response = await requestJson<EditorAssetFolderDeleteResponse>(
`${EDITOR_ASSET_API_BASE}/folders/${encodeURIComponent(folderId)}`,
{ method: 'DELETE' },
'删除图片画布素材文件夹失败',
);
return response.library;
}
export async function createEditorAsset(input: EditorAssetCreateInput) {
const response = await requestJson<EditorAssetResponse>(
EDITOR_ASSET_API_BASE,
jsonRequest('POST', input),
'创建图片画布素材失败',
);
return response.asset;
}
export async function updateEditorAsset(
assetId: string,
input: EditorAssetUpdateInput,
) {
const response = await requestJson<EditorAssetResponse>(
`${EDITOR_ASSET_API_BASE}/${encodeURIComponent(assetId)}`,
jsonRequest('PATCH', input),
'更新图片画布素材失败',
);
return response.asset;
}
export async function deleteEditorAsset(assetId: string) {
const response = await requestJson<EditorAssetResponse>(
`${EDITOR_ASSET_API_BASE}/${encodeURIComponent(assetId)}`,
{ method: 'DELETE' },
'删除图片画布素材失败',
);
return response.asset;
}
export async function generateEditorImage(input: EditorImageGenerationInput) {
return requestJson<EditorImageGenerationResponse>(
EDITOR_IMAGE_GENERATION_API,
jsonRequest('POST', {
prompt: input.prompt,
...(input.size ? { size: input.size } : {}),
...(input.kind ? { kind: input.kind } : {}),
...(input.model ? { model: input.model } : {}),
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
...(input.imageSize ? { imageSize: input.imageSize } : {}),
...(input.referenceImageSrcs?.length
? { referenceImageSrcs: input.referenceImageSrcs }
: {}),
}),
'生成图片失败',
{
timeoutMs: 1_200_000,
retry: {
maxRetries: 0,
},
},
);
}
export async function generateEditorIconSpritesheet(
input: EditorIconSpritesheetGenerationInput,
) {
return requestJson<EditorIconSpritesheetGenerationResponse>(
EDITOR_ICON_SPRITESHEET_GENERATION_API,
jsonRequest('POST', {
referenceImageSrc: input.referenceImageSrc,
iconDescriptions: input.iconDescriptions,
model: input.model?.trim() || EDITOR_IMAGE_MODEL_NANOBANANA2,
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
...(input.imageSize ? { imageSize: input.imageSize } : {}),
priceMudPoints: input.priceMudPoints,
}),
'生成图标素材失败',
{
timeoutMs: 1_200_000,
retry: {
maxRetries: 0,
},
},
);
}
export async function editEditorImage(input: EditorImageEditInput) {
return requestJson<EditorImageGenerationResponse>(
EDITOR_IMAGE_EDIT_API,
jsonRequest('POST', {
prompt: input.prompt,
sourceImageSrc: input.sourceImageSrc,
...(input.size ? { size: input.size } : {}),
...(input.model ? { model: input.model } : {}),
}),
'修改图片失败',
{
timeoutMs: 1_200_000,
retry: {
maxRetries: 0,
},
},
);
}
export async function generateEditorCharacterAnimation(
input: EditorCharacterAnimationGenerationInput,
) {
return requestJson<EditorCharacterAnimationGenerationResult>(
EDITOR_CHARACTER_ANIMATION_GENERATION_API,
jsonRequest('POST', input),
'生成角色动画失败',
{
timeoutMs: 1_200_000,
retry: {
maxRetries: 0,
},
},
);
}
export async function generateEditorVideo(input: EditorVideoGenerationInput) {
return requestJson<EditorVideoGenerationResponse>(
EDITOR_VIDEO_GENERATION_API,
jsonRequest('POST', {
prompt: input.prompt,
model: input.model,
aspectRatio: input.aspectRatio,
durationSeconds: input.durationSeconds,
resolution: input.resolution,
mode: input.mode,
sound: input.sound,
priceMudPoints: input.priceMudPoints,
...(input.referenceImageSrcs?.length
? { referenceImageSrcs: input.referenceImageSrcs }
: {}),
...(input.referenceVideoSrcs?.length
? { referenceVideoSrcs: input.referenceVideoSrcs }
: {}),
...(input.referenceAudioSrcs?.length
? { referenceAudioSrcs: input.referenceAudioSrcs }
: {}),
}),
'生成视频失败',
{
timeoutMs: 1_200_000,
retry: {
maxRetries: 0,
},
},
);
}
export async function generateEditorSoundEffect(
input: EditorSoundEffectGenerationInput,
) {
return requestJson<EditorAudioGenerationResponse>(
EDITOR_SOUND_EFFECT_GENERATION_API,
jsonRequest('POST', {
sound: input.sound,
type: input.type,
tempo: input.tempo,
priceMudPoints: input.priceMudPoints,
}),
'生成游戏音效失败',
{
timeoutMs: 1_200_000,
retry: {
maxRetries: 0,
},
},
);
}
export async function generateEditorBackgroundMusic(
input: EditorBackgroundMusicGenerationInput,
) {
return requestJson<EditorAudioGenerationResponse>(
EDITOR_BACKGROUND_MUSIC_GENERATION_API,
jsonRequest('POST', {
gptDescriptionPrompt: input.gptDescriptionPrompt,
makeInstrumental: input.makeInstrumental,
priceMudPoints: input.priceMudPoints,
}),
'生成游戏背景音乐失败',
{
timeoutMs: 1_200_000,
retry: {
maxRetries: 0,
},
},
);
}