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 & { 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) { 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( `${EDITOR_PROJECT_API_BASE}/recent`, { method: 'GET' }, '读取图片画布工程失败', ); } export async function createEditorProject( input: EditorProjectCreateInput = {}, ) { const response = await requestJson( 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( `${EDITOR_PROJECT_API_BASE}/${encodeURIComponent(projectId)}`, { method: 'GET' }, '读取图片画布工程失败', ); return response.project; } export async function renameEditorProject(projectId: string, title: string) { const response = await requestJson( `${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( `${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( `${EDITOR_PROJECT_API_BASE}/${encodeURIComponent(projectId)}/resources`, jsonRequest('POST', { ...input }), '创建图片画布资源失败', ); return response.resource; } export async function loadEditorAssetLibrary() { const response = await requestJson( `${EDITOR_ASSET_API_BASE}/library`, { method: 'GET' }, '读取图片画布素材库失败', ); return response.library; } export async function createEditorAssetFolder( label: string, sortOrder?: number, ) { const response = await requestJson( `${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( `${EDITOR_ASSET_API_BASE}/folders/${encodeURIComponent(folderId)}`, jsonRequest('PATCH', input), '更新图片画布素材文件夹失败', ); return response.folder; } export async function deleteEditorAssetFolder(folderId: string) { const response = await requestJson( `${EDITOR_ASSET_API_BASE}/folders/${encodeURIComponent(folderId)}`, { method: 'DELETE' }, '删除图片画布素材文件夹失败', ); return response.library; } export async function createEditorAsset(input: EditorAssetCreateInput) { const response = await requestJson( EDITOR_ASSET_API_BASE, jsonRequest('POST', input), '创建图片画布素材失败', ); return response.asset; } export async function updateEditorAsset( assetId: string, input: EditorAssetUpdateInput, ) { const response = await requestJson( `${EDITOR_ASSET_API_BASE}/${encodeURIComponent(assetId)}`, jsonRequest('PATCH', input), '更新图片画布素材失败', ); return response.asset; } export async function deleteEditorAsset(assetId: string) { const response = await requestJson( `${EDITOR_ASSET_API_BASE}/${encodeURIComponent(assetId)}`, { method: 'DELETE' }, '删除图片画布素材失败', ); return response.asset; } export async function generateEditorImage(input: EditorImageGenerationInput) { return requestJson( 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( 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( 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( EDITOR_CHARACTER_ANIMATION_GENERATION_API, jsonRequest('POST', input), '生成角色动画失败', { timeoutMs: 1_200_000, retry: { maxRetries: 0, }, }, ); } export async function generateEditorVideo(input: EditorVideoGenerationInput) { return requestJson( 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( 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( EDITOR_BACKGROUND_MUSIC_GENERATION_API, jsonRequest('POST', { gptDescriptionPrompt: input.gptDescriptionPrompt, makeInstrumental: input.makeInstrumental, priceMudPoints: input.priceMudPoints, }), '生成游戏背景音乐失败', { timeoutMs: 1_200_000, retry: { maxRetries: 0, }, }, ); }