Files
Genarrative/src/services/image-editor/editorProjectClient.ts
T
k88936 61fe4c48af
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 16s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 16s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 16s
Project CI / Native shell tests (pull_request) Failing after 16s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
图片画布接入 3D 资源:新增 model3d 类别、3D 预览与 3D 生成入口
- 后端资源投影按 asset_kind 分叉,新增 project_editor_client_media_src 与 EDITOR_MODEL3D_ASSET_KIND:model3d 只投影预览图,模型本体只留 objectKey,预览缺失才回落历史口径
- tripo3d 存储新增按字节魔数识别模型 content_type 与扩展名映射,不采信 provider 声明与下载响应头
- editor_project_storage 把 model3d 加入 EDITOR_CANVAS_ASSET_KINDS 白名单
- 前端 CanvasAssetKind 与 image-canvas-core 类型新增 model3d:画布卡片渲染预览图或 3D 占位,类型角标新增「3D模型」,覆盖资格只在它与空类别之间成立
- 新增 model3d-preview 子目录(Model3dViewerModal / Model3dViewerModel),Tailwind 内联、不新增 CSS,失败原因分超限 / 格式不可识别 / 无 WebGL2 三类展示
- 选中工具条新增只读 preview-model3d 能力位,仅 objectKey 为空时置灰;打开预览同时选中图层并暂停画布快捷键与舞台交互
- 查看器包源对象 format 改为可选,新增「声明 Content-Type → 字节魔数 → 地址扩展名」三级格式判定并导出支持格式清单
- 导出工作流新增 3d_models/ 目录,模型下载按真实响应头 MIME 定扩展名、对象键扩展名兜底
- 新增 3D 生成入口:底部工具栏浮动子选项与文生 / 图生 3D 面板,落 projectResource + canvasCompletion,复用占位框与任务侧栏
- 3D 定价只读实时定价查询,段缺失即入口不可提交;幂等键由前端铸造,用户重试必须换键
- 新增 ADR 0003 / 0004 与实施计划、里程碑文档;更新 Tripo 技术方案、后端架构、CONTEXT 术语、decision-log 与 pitfalls
- .gitignore 忽略仓库根目录手工下载的 3D 样例模型
2026-09-22 10:10:00 +08:00

1786 lines
56 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
type BackgroundMusicPromptAssistRequest,
type BackgroundMusicPromptAssistResponse,
type EditorSoundEffectGenerationMetadataV2,
type EditorSoundEffectGenerationRequest,
SOUND_EFFECT_DURATION_MAX_SECONDS,
SOUND_EFFECT_DURATION_MIN_SECONDS,
type SoundEffectPromptOptimizeRequest,
type SoundEffectPromptOptimizeResponse,
} from '../../../packages/shared/src/contracts/editorAudio';
import type { EditorSceneGenerationRequest } from '../../../packages/shared/src/contracts/editorScene';
import type { ExternalGenerationJobStatusRecord } from '../../../packages/shared/src/contracts/externalGeneration';
import { requestJson } from '../apiClient';
import { EDITOR_GENERATION_REQUEST_RETRY_OPTIONS } from './editorRetryOptions';
const EDITOR_PROJECT_API_BASE = '/api/editor/projects';
const EDITOR_ASSET_API_BASE = '/api/editor/assets';
const EDITOR_SHOWCASE_RESOURCE_API = '/api/editor/showcase/resources';
const EDITOR_SHOWCASE_ASSET_API_BASE = '/api/editor/showcase/assets';
const EDITOR_PROJECT_RESOURCE_API_BASE = '/api/editor/project-resources';
const EDITOR_IMAGE_GENERATION_API = '/api/editor/images/generations';
const EDITOR_SCENE_GENERATION_API = '/api/editor/scenes/generations';
const EDITOR_ICON_SPEC_GENERATION_API = '/api/editor/icon-specs/generations';
const EDITOR_ICON_SPEC_REFINE_GAME_PLAY_API =
'/api/editor/llm/icon-specs/refine-game-play';
const EDITOR_ICON_SPEC_REFINE_ART_STYLE_API =
'/api/editor/llm/icon-specs/refine-art-style';
export const EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH = 200;
export const EDITOR_ICON_DESCRIPTION_LIMIT = 100;
export const EDITOR_ICON_DESCRIPTION_MAX_CHARS = 200;
export const EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_CHARS = 2_000;
export const EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_UTF8_BYTES = 6 * 1024;
const EDITOR_IMAGE_EDIT_API = '/api/editor/images/edits';
const EDITOR_BACKGROUND_REMOVAL_API = '/api/editor/images/background-removals';
const EDITOR_PIXEL_ART_SNAP_API = '/api/editor/images/pixel-art-snaps';
const EDITOR_ICON_SPRITESHEET_GENERATION_API =
'/api/editor/icon-spritesheets/generations';
const EDITOR_ICON_SPRITESHEET_SLICE_API =
'/api/editor/icon-spritesheets/slices';
const EDITOR_UI_DESIGN_ASSET_EXTRACTION_API =
'/api/editor/ui-designs/assets/extractions';
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_BACKGROUND_MUSIC_PROMPT_COMPLETION_API =
'/api/editor/audios/background-music/prompts/completions';
const EDITOR_BACKGROUND_MUSIC_PROMPT_SIMPLIFICATION_API =
'/api/editor/audios/background-music/prompts/simplifications';
const EDITOR_SOUND_EFFECT_PROMPT_OPTIMIZATION_API =
'/api/editor/audios/sound-effects/prompts/optimizations';
/**
* 覆盖简化两个业务语义轮加单轮 transport retry 的服务端预算,同时为浏览器到 BFF 的
* 悬挂连接提供有界退出;超时后走既有失败路径恢复 idle,不写回候选。
*/
/** 3D 提交只做校验与入队,毫秒级返回,不沿用生成链路的分钟级超时。 */
export const EDITOR_MODEL3D_SUBMISSION_TIMEOUT_MS = 60_000;
export const EDITOR_BACKGROUND_MUSIC_PROMPT_ASSIST_TIMEOUT_MS = 180_000;
export const EDITOR_SOUND_EFFECT_PROMPT_OPTIMIZE_TIMEOUT_MS = 180_000;
const EDITOR_GENERATION_PRICING_API = '/api/editor/generation-pricing';
const EDITOR_MODEL3D_TEXT_TO_MODEL_API = '/api/assets/tripo/text-to-model';
const EDITOR_MODEL3D_IMAGE_TO_MODEL_API = '/api/assets/tripo/image-to-model';
const EDITOR_IMAGE_MODEL_NANOBANANA2 = 'gemini-3.1-flash-image-preview';
const EDITOR_IMAGE_REFERENCE_LIMIT = 5;
const EDITOR_QUICK_EDIT_REFERENCE_LIMIT = 9;
const EDITOR_IMAGE_EDIT_EXTRA_REFERENCE_LIMIT = 8;
const EDITOR_ICON_EXTRA_REFERENCE_LIMIT = 8;
const EDITOR_UI_EXTRA_REFERENCE_LIMIT = 5;
const EDITOR_VIDEO_REFERENCE_REQUEST_LIMIT_BYTES = 256 * 1024;
const DEFAULT_PROJECT_TITLE = '未命名画布';
function assertStableEditorMediaReference(value: string, fieldLabel: string) {
const normalized = value.trimStart().toLowerCase();
if (normalized.startsWith('data:') || normalized.startsWith('blob:')) {
throw new Error(
`${fieldLabel}必须先上传 OSS,并使用 objectKey、项目资源、素材 ID 或支持的 URL 引用。`,
);
}
}
function assertStableEditorMediaReferences(
values: readonly string[] | undefined,
fieldLabel: string,
) {
values?.forEach((value) =>
assertStableEditorMediaReference(value, fieldLabel),
);
}
function requireEditorIconSpecPromptLength(value: string, fieldLabel: string) {
const normalized = value.trim();
if (!normalized) {
throw new Error(`${fieldLabel}不能为空`);
}
if (Array.from(normalized).length > EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH) {
throw new Error(
`${fieldLabel}不能超过 ${EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH} 个字符`,
);
}
return normalized;
}
function resolveEditorProviderReferenceLimit(model: string | null | undefined) {
const normalized = model?.trim();
return normalized === EDITOR_IMAGE_MODEL_NANOBANANA2 ||
normalized === 'nanobanana2' ||
normalized === 'nano-banana'
? 14
: 5;
}
function assertEditorReferenceLimit(
values: readonly string[] | undefined,
limit: number,
fieldLabel: string,
) {
const count = values?.filter((value) => value.trim()).length ?? 0;
if (count > limit) {
throw new Error(`${fieldLabel}最多允许 ${limit} 张,当前选择 ${count} 张`);
}
}
function normalizeEditorIconDescriptions(values: readonly string[]) {
const normalized = values.map((value) => value.trim()).filter(Boolean);
if (
normalized.length < 1 ||
normalized.length > EDITOR_ICON_DESCRIPTION_LIMIT
) {
throw new Error(
`图标素材描述数量必须在 1 到 ${EDITOR_ICON_DESCRIPTION_LIMIT} 个之间`,
);
}
normalized.forEach((description, index) => {
const actualLength = Array.from(description).length;
if (actualLength > EDITOR_ICON_DESCRIPTION_MAX_CHARS) {
throw new Error(
`第 ${index + 1} 条图标素材描述不能超过 ${EDITOR_ICON_DESCRIPTION_MAX_CHARS} 个字符`,
);
}
});
const prompt = normalized.join('\n');
const totalLength = Array.from(prompt).length;
if (totalLength > EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_CHARS) {
throw new Error(
`图标素材描述合计不能超过 ${EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_CHARS} 个字符`,
);
}
const totalUtf8Bytes = new TextEncoder().encode(prompt).byteLength;
if (totalUtf8Bytes > EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_UTF8_BYTES) {
throw new Error(
`图标素材描述合计不能超过 ${EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_UTF8_BYTES} 个 UTF-8 字节`,
);
}
return normalized;
}
function assertStableEditorVideoReferences(input: EditorVideoGenerationInput) {
const references = [
...(input.referenceImageSrcs ?? []),
...(input.referenceVideoSrcs ?? []),
...(input.referenceAudioSrcs ?? []),
];
assertStableEditorMediaReferences(references, '视频参考素材');
const requestBytes = new TextEncoder().encode(references.join('')).byteLength;
if (requestBytes > EDITOR_VIDEO_REFERENCE_REQUEST_LIMIT_BYTES) {
throw new Error('视频参考素材稳定引用字段总长度不能超过 256KB');
}
}
export type EditorCanvasViewport = {
x: number;
y: number;
scale: number;
};
export type EditorProjectLayerSnapshot = Record<string, unknown> & {
layerId: string;
resourceId: string;
assetKindOverride?: string | null;
};
export type EditorAssetGenerationInputField = {
id?: string;
title: string;
value: string | number | boolean;
};
export type EditorAssetGenerationInputReference = {
id?: string;
title: string;
label?: string;
refType: 'project-resource' | 'asset';
refId: string;
};
export type EditorAssetGenerationInputs = {
version?: 2;
action?:
| 'image.generate'
| 'scene.generate'
| 'spec.generate'
| 'character.generate'
| 'icon.generate'
| 'ui-design.generate'
| 'publication.generate'
| 'video.generate'
| 'audio.sound-effect.generate'
| 'audio.background-music.generate'
| 'character-animation.generate'
| 'image.edit'
| 'ui-design.extract-assets'
| 'image.perfect-pixel'
| 'spritesheet.split'
| 'image.remove-background'
| 'image.crop-expand';
fields: EditorAssetGenerationInputField[];
references: EditorAssetGenerationInputReference[];
soundEffect?: EditorSoundEffectGenerationMetadataV2;
[key: string]: unknown;
};
export type EditorProjectResourceSourceType =
| 'uploaded'
| 'generated'
| 'mock_generated';
export type EditorProjectResourceSnapshot = {
resourceId: string;
showcaseId?: string | null;
assetId?: string | null;
label?: string | null;
projectId: string;
ownerUserId?: string | null;
authorDisplayName?: string | null;
authorPublicUserCode?: string | null;
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;
imageSequenceFrames?: EditorImageSequenceFrameResult[] | null;
imageSequenceDurationMs?: number | null;
sourceResourceId?: string | null;
assetKind?: string | null;
showcaseCategory?: string | null;
generationInputs?: EditorAssetGenerationInputs | null;
publicShowcaseEnabled?: boolean;
reviewStatus?: 'pending' | 'approved' | 'rejected' | string | null;
displayEnabled?: boolean | null;
likeCount?: number | null;
generationCostMudPoints?: number;
refundMudPoints?: number | null;
createdAt?: string;
updatedAt?: string;
};
export type EditorPublicShowcaseResourceSnapshot =
EditorProjectResourceSnapshot & {
viewerLiked: boolean;
};
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;
thumbnailSrc?: string | null;
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;
imageSequenceFrames?: EditorImageSequenceFrameResult[] | null;
imageSequenceDurationMs?: number | null;
sourceResourceId?: string | null;
assetKind?: string | null;
showcaseCategory?: string | null;
generationInputs?: EditorAssetGenerationInputs | null;
publicShowcaseEnabled?: boolean | null;
generationCostMudPoints?: number;
showcaseId?: string | null;
showcaseReviewStatus?: 'pending' | 'approved' | 'rejected' | string | null;
showcaseDisplayEnabled?: boolean | null;
showcaseLikeCount?: number | null;
createdAt?: string;
updatedAt?: string;
};
export type EditorShowcaseCampaignSnapshot = {
enabled: boolean;
title: string;
imageSrc: string;
imageObjectKey?: string | null;
imageWidth?: number | null;
imageHeight?: number | null;
prompt: string;
author: string;
costText: string;
updatedAt?: string;
};
export type EditorAssetLibrarySnapshot = {
folders: EditorAssetFolderSnapshot[];
assets: EditorAssetSnapshot[];
};
export type EditorImageGenerationStyle = 'none' | 'pixelArt';
export type EditorImageGenerationInput = {
prompt: string;
size?: string;
kind?:
| 'spec'
| 'character'
| 'quick-edit'
| 'ui-design'
| 'publication-material';
model?: string;
screenColor?: string;
segModel?: string;
style?: EditorImageGenerationStyle;
aspectRatio?: string;
imageSize?: string;
referenceImageSrcs?: string[];
projectId?: string | null;
assetKind?: string | null;
generationInputs?: EditorAssetGenerationInputs | null;
assetFolderId?: string | null;
assetLabel?: string | null;
sourceResourceId?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
};
export type EditorIconSpecGenerationInput = {
playSetting: string;
artStyle: string;
referenceId?: string;
projectId?: string | null;
generationInputs?: EditorAssetGenerationInputs | null;
assetFolderId?: string | null;
assetLabel?: string | null;
sourceResourceId?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
};
export type EditorCanvasGenerationCompletionInput = {
dialogId?: string;
title: string;
placeholder: {
x: number;
y: number;
width: number;
height: number;
originalWidth: number;
originalHeight: number;
};
};
export type EditorIconSpritesheetGenerationInput = {
referenceId: string;
referenceImageSrcs?: string[];
iconDescriptions: string[];
/** 必填且没有默认值:必须显式声明连通域或网格切分。 */
sliceMode: 'connected-components' | 'grid';
gridX?: number;
gridY?: number;
model?: string;
screenColor?: string;
segModel?: string;
style?: EditorImageGenerationStyle;
aspectRatio?: string;
imageSize?: string;
projectId?: string | null;
generationInputs?: EditorAssetGenerationInputs | null;
assetFolderId?: string | null;
assetLabel?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
};
export type EditorIconSpritesheetSliceInput = {
projectId: string;
sourceLayerId: string;
sourceResourceId: string;
assetFolderId?: string | null;
canvasCompletion: EditorCanvasGenerationCompletionInput;
};
export type EditorUiDesignAssetExtractionInput = {
sourceImageSrc: string;
model?: string;
screenColor?: string;
segModel?: string;
referenceImageSrcs?: string[];
aspectRatio?: string;
imageSize?: string;
projectId?: string | null;
generationInputs?: EditorAssetGenerationInputs | null;
assetFolderId?: string | null;
spritesheetLabel?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
};
export type EditorImageEditInput = {
prompt: string;
sourceReferenceId: string;
size?: string;
model?: string;
aspectRatio?: string;
imageSize?: string;
referenceImageSrcs?: string[];
projectId?: string | null;
generationInputs?: EditorAssetGenerationInputs | null;
assetFolderId?: string | null;
assetLabel?: string | null;
targetLayerId?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
};
export type EditorBackgroundRemovalInput = {
sourceImageSrc: string;
projectId?: string | null;
targetLayerId?: string | null;
assetKind?: string | null;
generationInputs?: EditorAssetGenerationInputs | null;
assetFolderId?: string | null;
assetLabel?: string | null;
sourceResourceId?: string | null;
taskId?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
};
export type EditorPixelArtSnapInput = {
sourceImageSrc: string;
projectId: string;
sourceResourceId?: string | null;
assetKind?: string | null;
generationInputs?: EditorAssetGenerationInputs | null;
assetFolderId?: string | null;
assetLabel?: string | null;
canvasCompletion: EditorCanvasGenerationCompletionInput & {
dialogId: 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;
taskId: string;
resource?: EditorProjectResourceSnapshot | null;
asset?: EditorAssetSnapshot | null;
project?: EditorProjectSnapshot | null;
warning?: EditorGenerationWarning | null;
queueState?: ExternalGenerationJobStatusRecord | null;
};
export type EditorBackgroundRemovalResult = {
queueState: ExternalGenerationJobStatusRecord;
};
export type EditorPixelArtSnapResult = {
imageSrc: string;
objectKey: string;
assetObjectId: string;
width: number;
height: number;
sourceType: 'generated';
taskId: string;
elapsedMs: number;
provider: 'Genarrative';
resource: EditorProjectResourceSnapshot;
asset: EditorAssetSnapshot;
project: EditorProjectSnapshot | null;
};
export type EditorIconSpritesheetIconResult = {
name: string;
imageSrc: string;
width: number;
height: number;
resource?: EditorProjectResourceSnapshot | null;
asset?: EditorAssetSnapshot | null;
};
export type EditorGenerationWarning = {
code: string;
reason: string;
};
export type EditorIconSpritesheetSliceWarning = EditorGenerationWarning;
export type EditorIconSpritesheetGenerationResult = {
spritesheetImageSrc: string;
spritesheetWidth: number;
spritesheetHeight: number;
iconImageSrcs: EditorIconSpritesheetIconResult[];
sliceMode?: 'connected-components' | 'grid';
gridX?: number;
gridY?: number;
sliceWarning?: EditorIconSpritesheetSliceWarning | null;
prompt: string;
actualPrompt?: string | null;
model: string;
taskId: string;
priceMudPoints: number;
spritesheetResource?: EditorProjectResourceSnapshot | null;
spritesheetAsset?: EditorAssetSnapshot | null;
project?: EditorProjectSnapshot | null;
warning?: EditorGenerationWarning | null;
queueState?: ExternalGenerationJobStatusRecord | null;
};
export type EditorIconSpritesheetSliceResult = {
iconImageSrcs: EditorIconSpritesheetIconResult[];
project: EditorProjectSnapshot;
};
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;
/** 纯色抠像背景色(hex 或 'auto'),继承生图的背景色选项行为。 */
screenColor?: string;
resolution: EditorCharacterAnimationResolution;
ratio: EditorCharacterAnimationRatio;
frameCount: EditorCharacterAnimationFrameCount;
durationSeconds: EditorCharacterAnimationDurationSeconds;
model: 'seedance2.0-fast';
projectId?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
generationInputs?: EditorAssetGenerationInputs | null;
sourceResourceId?: string | null;
assetFolderId?: string | null;
assetLabel?: string | null;
};
export type EditorImageSequenceFrameResult = {
imageSrc: string;
objectKey?: string | null;
assetObjectId?: string | null;
width: number;
height: number;
};
export type EditorCharacterAnimationGenerationResult = {
taskId: string;
model: 'seedance2.0-fast';
prompt: string;
previewVideoPath: string;
frames: EditorImageSequenceFrameResult[];
frameCount: number;
durationSeconds: number;
fps: number;
priceMudPoints: number;
project?: EditorProjectSnapshot | null;
resource?: EditorProjectResourceSnapshot | null;
asset?: EditorAssetSnapshot | null;
queueState?: ExternalGenerationJobStatusRecord | null;
};
export type EditorVideoModel =
| 'seedance2.0'
| 'seedance2.0-fast'
| 'kling3.0'
| 'kling3.0-omni'
| 'veo3.1'
| 'veo3.1-fast';
export type EditorVideoResolution = '480p' | '720p' | '1080p';
export type EditorVideoSoundMode = 'on' | 'off';
export type EditorVideoAspectRatio =
| '16:9'
| '9:16'
| '1:1'
| '4:3'
| '3:4'
| '21:9';
export type EditorVideoGenerationInput = {
prompt: string;
model: EditorVideoModel;
aspectRatio: EditorVideoAspectRatio;
durationSeconds: number;
resolution: EditorVideoResolution;
mode: 'std';
sound: EditorVideoSoundMode;
webSearchEnabled?: boolean;
referenceImageSrcs?: string[];
referenceVideoSrcs?: string[];
referenceAudioSrcs?: string[];
projectId?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
generationInputs?: EditorAssetGenerationInputs | null;
sourceResourceId?: string | null;
assetKind?: string | null;
assetFolderId?: string | null;
assetLabel?: string | null;
};
export type EditorVideoGenerationResult = {
videoSrc: string;
thumbnailSrc?: string | null;
objectKey?: string | null;
assetObjectId?: string | null;
width: number;
height: number;
sourceType: 'generated';
prompt: string;
actualPrompt?: string | null;
model: string;
taskId: string;
durationSeconds: number;
priceMudPoints?: number;
resource?: EditorProjectResourceSnapshot | null;
asset?: EditorAssetSnapshot | null;
project?: EditorProjectSnapshot | null;
queueState?: ExternalGenerationJobStatusRecord | null;
};
export type EditorSoundEffectGenerationInput =
EditorSoundEffectGenerationRequest & {
projectId?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
generationInputs?: EditorAssetGenerationInputs | null;
assetFolderId?: string | null;
assetLabel?: string | null;
};
export type EditorBackgroundMusicGenerationInput = {
gptDescriptionPrompt: string;
makeInstrumental: true;
projectId?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
generationInputs?: EditorAssetGenerationInputs | null;
assetFolderId?: string | null;
assetLabel?: string | null;
};
export type EditorBackgroundMusicPromptAssistOptions = {
signal?: AbortSignal;
};
export type EditorSoundEffectPromptOptimizeOptions = {
signal?: AbortSignal;
};
export type EditorAudioGenerationResult = {
audioSrc: string;
objectKey?: string | null;
assetObjectId?: string | null;
width: number;
height: number;
sourceType: 'generated';
prompt: string;
actualPrompt?: string | null;
model: string;
taskId: string;
priceMudPoints: number;
audioKind: 'sound-effect' | 'background-music';
durationSeconds?: number | null;
loop?: boolean | null;
resource?: EditorProjectResourceSnapshot | null;
asset?: EditorAssetSnapshot | null;
project?: EditorProjectSnapshot | null;
queueState?: ExternalGenerationJobStatusRecord | null;
};
export type EditorGenerationPricingUnit = 'perGeneration' | 'perSecond';
export type EditorGenerationModelPricing = {
unit: EditorGenerationPricingUnit;
price?: number;
prices?: Record<string, number>;
};
export type Model3dPricingEndpoint = 'text-to-model' | 'image-to-model';
export type Model3dAddOnPriceKey =
| 'hdTexture'
| 'ultraTexture'
| 'hdGeometry'
| 'quadMesh'
| 'smartLowPoly'
| 'generateParts';
/**
* 3D 生成的定价段:底价按 `endpoint × modelVersion × 是否有贴图` 拆分,
* add-on 按请求参数判定后叠加。整段缺失即 3D 生成不可用(后端 fail closed),
* 前端不内置任何兜底数值。
*/
export type Model3dPricingConfig = {
basePrices?: Record<
Model3dPricingEndpoint,
Record<string, { noTexture: number; texture: number }>
> | null;
addOnPrices?: Partial<Record<Model3dAddOnPriceKey, number>> | null;
};
export type EditorGenerationPricingConfig = {
models: Record<string, EditorGenerationModelPricing>;
model3d?: Model3dPricingConfig | 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[];
revision?: number;
layoutStorageVersion?: number;
backgroundColor?: string | null;
createdAt?: string;
updatedAt: string;
};
export type EditorProjectLoadOptions = {
signal?: AbortSignal;
timeoutMs?: number;
deadlineAt?: number;
};
export type EditorProjectCreateInput = {
title?: string;
};
export type EditorProjectLayoutSaveInput = {
viewport: EditorCanvasViewport;
layers: EditorProjectLayerSnapshot[];
expectedRevision: number;
};
export type EditorProjectLayoutSaveResult = {
projectId: string;
canvasId: string;
revision: number;
updatedAt: string;
};
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;
imageSequenceFrames?: EditorImageSequenceFrameResult[] | null;
imageSequenceDurationMs?: number | null;
sourceResourceId?: string | null;
assetKind?: string | null;
generationInputs?: EditorAssetGenerationInputs | 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;
imageSequenceFrames?: EditorImageSequenceFrameResult[] | null;
imageSequenceDurationMs?: number | null;
assetKind?: string | null;
generationInputs?: EditorAssetGenerationInputs | null;
};
export type EditorAssetUpdateInput = {
label?: string;
folderId?: string;
};
type EditorProjectResponse = {
project: EditorProjectSnapshot;
};
type EditorProjectLayoutSaveResponse = EditorProjectLayoutSaveResult;
type EditorProjectRecentResponse = {
project: EditorProjectSnapshot | null;
};
type EditorProjectResourceResponse = {
resource: EditorProjectResourceSnapshot;
};
type EditorProjectResourceListResponse = {
resources: EditorPublicShowcaseResourceSnapshot[];
nextCursor?: string | null;
campaign?: EditorShowcaseCampaignSnapshot | null;
};
export type EditorProjectResourceListPage = {
resources: EditorPublicShowcaseResourceSnapshot[];
nextCursor: string | null;
campaign?: EditorShowcaseCampaignSnapshot | null;
};
type EditorAssetLibraryResponse = {
library: EditorAssetLibrarySnapshot;
};
type EditorAssetFolderResponse = {
folder: EditorAssetFolderSnapshot;
};
type EditorAssetFolderDeleteResponse = {
library: EditorAssetLibrarySnapshot;
};
type EditorAssetResponse = {
asset: EditorAssetSnapshot;
};
type EditorShowcaseAssetResponse = {
showcaseAsset: EditorProjectResourceSnapshot;
};
type EditorShowcaseAssetViewerResponse = {
showcaseAsset: EditorPublicShowcaseResourceSnapshot;
};
type EditorImageGenerationResponse = EditorImageGenerationResult;
type EditorBackgroundRemovalResponse = EditorBackgroundRemovalResult;
type EditorPixelArtSnapResponse = EditorPixelArtSnapResult;
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 listPublicEditorProjectResources({
cursor,
viewer = 'anonymous',
}: {
cursor?: string | null;
viewer?: 'anonymous' | 'authenticated';
} = {}): Promise<EditorProjectResourceListPage> {
const params = new URLSearchParams();
if (cursor?.trim()) {
params.set('cursor', cursor.trim());
}
const url = params.toString()
? `${EDITOR_SHOWCASE_RESOURCE_API}?${params.toString()}`
: EDITOR_SHOWCASE_RESOURCE_API;
const response =
viewer === 'authenticated'
? await requestJson<EditorProjectResourceListResponse>(
url,
{ method: 'GET' },
'读取陶泥儿精选素材失败',
)
: await requestJson<EditorProjectResourceListResponse>(
url,
{ method: 'GET' },
'读取陶泥儿精选素材失败',
{
skipAuth: true,
skipRefresh: true,
notifyAuthStateChange: false,
clearAuthOnUnauthorized: false,
},
);
return {
resources: response.resources,
nextCursor: response.nextCursor?.trim() || null,
campaign: response.campaign ?? null,
};
}
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 loadEditorGenerationPricing() {
return requestJson<EditorGenerationPricingConfig>(
EDITOR_GENERATION_PRICING_API,
{ method: 'GET' },
'读取模型定价失败',
{
skipAuth: true,
skipRefresh: true,
notifyAuthStateChange: false,
clearAuthOnUnauthorized: false,
},
);
}
export async function loadEditorProject(
projectId: string,
options: EditorProjectLoadOptions = {},
) {
const hasAbsoluteDeadline =
typeof options.deadlineAt === 'number' &&
Number.isFinite(options.deadlineAt);
const response = await requestJson<EditorProjectResponse>(
`${EDITOR_PROJECT_API_BASE}/${encodeURIComponent(projectId)}`,
{
method: 'GET',
...(options.signal ? { signal: options.signal } : {}),
},
'读取图片画布工程失败',
hasAbsoluteDeadline
? { deadlineAt: options.deadlineAt }
: // 中文注释:普通项目读取继续保留既有单次 fetch timeout;完美像素对账显式传
// deadlineAt,改由 requestJson 从鉴权恢复到响应体读取约束完整生命周期。
{ timeoutMs: options.timeoutMs ?? 60_000 },
);
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;
}
// 中文注释:这里曾接受第三参数(调用方注入的 `signal` / `deadlineAt`),只服务于已删除的
// 严格布局保存通道;该通道删除后全部调用点都只传两个参数,注入分支恒不生效。一并摘掉,
// 免得后来者以为调用方还能控制这次保存的取消与截止。
export async function saveEditorProjectLayout(
projectId: string,
input: EditorProjectLayoutSaveInput,
) {
return requestJson<EditorProjectLayoutSaveResponse>(
`${EDITOR_PROJECT_API_BASE}/${encodeURIComponent(projectId)}`,
jsonRequest('PATCH', {
viewport: input.viewport,
layers: input.layers,
expectedRevision: input.expectedRevision,
}),
'保存图片画布工程失败',
// 布局保存会被生成提交链同步等待,必须连鉴权恢复和响应体读取一起有界。
{ deadlineAt: Date.now() + 60_000 },
);
}
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 updateEditorProjectResourceShowcase(
resourceId: string,
publicShowcaseEnabled: boolean,
) {
const response = await requestJson<EditorProjectResourceResponse>(
`${EDITOR_PROJECT_RESOURCE_API_BASE}/${encodeURIComponent(resourceId)}/showcase`,
jsonRequest('PATCH', { publicShowcaseEnabled }),
'更新素材公开展示状态失败',
);
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 submitEditorAssetShowcase(assetId: string) {
const response = await requestJson<EditorShowcaseAssetResponse>(
`${EDITOR_ASSET_API_BASE}/${encodeURIComponent(assetId)}/showcase-submissions`,
{ method: 'POST' },
'提交精选审核失败',
);
return response.showcaseAsset;
}
export async function toggleEditorShowcaseAssetLike(
showcaseId: string,
liked: boolean,
) {
const response = await requestJson<EditorShowcaseAssetViewerResponse>(
`${EDITOR_SHOWCASE_ASSET_API_BASE}/${encodeURIComponent(showcaseId)}/likes`,
jsonRequest('POST', { liked }),
liked ? '点赞精选素材失败' : '取消点赞精选素材失败',
);
return response.showcaseAsset;
}
export type EditorSceneGenerationInput = EditorSceneGenerationRequest;
export async function generateEditorScene(input: EditorSceneGenerationInput) {
assertStableEditorMediaReferences(input.referenceImageSrcs, '场景参考图');
assertEditorReferenceLimit(
input.referenceImageSrcs,
EDITOR_IMAGE_REFERENCE_LIMIT,
'场景参考图',
);
return requestJson<EditorImageGenerationResponse>(
EDITOR_SCENE_GENERATION_API,
jsonRequest('POST', {
sceneContent: input.sceneContent,
stylePreset: input.stylePreset,
...(input.customStyle ? { customStyle: input.customStyle } : {}),
...(input.model ? { model: input.model } : {}),
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
...(input.imageSize ? { imageSize: input.imageSize } : {}),
...(input.referenceImageSrcs?.length
? { referenceImageSrcs: input.referenceImageSrcs }
: {}),
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),
}),
'生成游戏场景失败',
{
timeoutMs: 1_200_000,
retry: EDITOR_GENERATION_REQUEST_RETRY_OPTIONS,
},
);
}
export async function generateEditorImage(input: EditorImageGenerationInput) {
assertStableEditorMediaReferences(input.referenceImageSrcs, '生成参考图');
const referenceLimit =
input.kind === 'quick-edit'
? Math.min(
EDITOR_QUICK_EDIT_REFERENCE_LIMIT,
resolveEditorProviderReferenceLimit(input.model),
)
: EDITOR_IMAGE_REFERENCE_LIMIT;
assertEditorReferenceLimit(
input.referenceImageSrcs,
referenceLimit,
'生成参考图',
);
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.screenColor ? { screenColor: input.screenColor } : {}),
...(input.segModel ? { segModel: input.segModel } : {}),
...(input.style ? { style: input.style } : {}),
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
...(input.imageSize ? { imageSize: input.imageSize } : {}),
...(input.referenceImageSrcs?.length
? { referenceImageSrcs: input.referenceImageSrcs }
: {}),
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.assetKind ? { assetKind: input.assetKind } : {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
...(input.sourceResourceId
? { sourceResourceId: input.sourceResourceId }
: {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),
}),
'生成图片失败',
{
timeoutMs: 1_200_000,
retry: EDITOR_GENERATION_REQUEST_RETRY_OPTIONS,
},
);
}
export async function refineEditorIconSpecPlaySetting(playSetting: string) {
const validPlaySetting = requireEditorIconSpecPromptLength(
playSetting,
'玩法设定',
);
const response = await requestJson<{ playSetting: string }>(
EDITOR_ICON_SPEC_REFINE_GAME_PLAY_API,
jsonRequest('POST', { playSetting: validPlaySetting }),
'优化玩法设定失败',
);
return requireEditorIconSpecPromptLength(response.playSetting, '玩法设定');
}
export async function refineEditorIconSpecArtStyle(artStyle: string) {
const validArtStyle = requireEditorIconSpecPromptLength(artStyle, '美术风格');
const response = await requestJson<{ artStyle: string }>(
EDITOR_ICON_SPEC_REFINE_ART_STYLE_API,
jsonRequest('POST', { artStyle: validArtStyle }),
'优化美术风格失败',
);
return requireEditorIconSpecPromptLength(response.artStyle, '美术风格');
}
export async function generateEditorIconSpec(
input: EditorIconSpecGenerationInput,
) {
const playSetting = requireEditorIconSpecPromptLength(
input.playSetting,
'玩法设定',
);
const artStyle = requireEditorIconSpecPromptLength(
input.artStyle,
'美术风格',
);
return requestJson<EditorImageGenerationResponse>(
EDITOR_ICON_SPEC_GENERATION_API,
jsonRequest('POST', {
playSetting,
artStyle,
...(input.referenceId ? { referenceId: input.referenceId } : {}),
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
...(input.sourceResourceId
? { sourceResourceId: input.sourceResourceId }
: {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),
}),
'生成图标规范失败',
{
timeoutMs: 1_200_000,
},
);
}
export async function generateEditorIconSpritesheet(
input: EditorIconSpritesheetGenerationInput,
) {
const iconDescriptions = normalizeEditorIconDescriptions(
input.iconDescriptions,
);
const model = input.model?.trim() || EDITOR_IMAGE_MODEL_NANOBANANA2;
// 切分模式没有默认值:声明必须自洽,网格尺寸只能与 grid 同时提交。
if (input.sliceMode === 'grid') {
if (input.gridX === undefined || input.gridY === undefined) {
throw new Error('sliceMode=grid 必须同时提供 gridX 与 gridY');
}
} else if (input.gridX !== undefined || input.gridY !== undefined) {
throw new Error(
'sliceMode=connected-components 不接受 gridX/gridY:网格尺寸只能与 grid 同时提交',
);
}
assertStableEditorMediaReferences(input.referenceImageSrcs, '图标素材参考图');
assertEditorReferenceLimit(
input.referenceImageSrcs,
Math.min(
EDITOR_ICON_EXTRA_REFERENCE_LIMIT,
resolveEditorProviderReferenceLimit(model) - 1,
),
'图标素材参考图',
);
return requestJson<EditorIconSpritesheetGenerationResponse>(
EDITOR_ICON_SPRITESHEET_GENERATION_API,
jsonRequest('POST', {
referenceId: input.referenceId,
...(input.referenceImageSrcs?.length
? { referenceImageSrcs: input.referenceImageSrcs }
: {}),
iconDescriptions,
sliceMode: input.sliceMode,
...(input.sliceMode === 'grid' && input.gridX !== undefined
? { gridX: input.gridX }
: {}),
...(input.sliceMode === 'grid' && input.gridY !== undefined
? { gridY: input.gridY }
: {}),
model,
...(input.screenColor ? { screenColor: input.screenColor } : {}),
...(input.segModel ? { segModel: input.segModel } : {}),
...(input.style ? { style: input.style } : {}),
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
...(input.imageSize ? { imageSize: input.imageSize } : {}),
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),
}),
'生成图标素材失败',
{
timeoutMs: 1_200_000,
retry: EDITOR_GENERATION_REQUEST_RETRY_OPTIONS,
},
);
}
export async function splitEditorIconSpritesheet(
input: EditorIconSpritesheetSliceInput,
) {
return requestJson<EditorIconSpritesheetSliceResult>(
EDITOR_ICON_SPRITESHEET_SLICE_API,
jsonRequest('POST', {
projectId: input.projectId,
sourceLayerId: input.sourceLayerId,
sourceResourceId: input.sourceResourceId,
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
canvasCompletion: input.canvasCompletion,
}),
'拆分图集失败',
);
}
export async function extractEditorUiDesignAssets(
input: EditorUiDesignAssetExtractionInput,
) {
assertStableEditorMediaReference(input.sourceImageSrc, 'UI设计图');
assertStableEditorMediaReferences(input.referenceImageSrcs, 'UI素材参考图');
assertEditorReferenceLimit(
input.referenceImageSrcs,
Math.min(
EDITOR_UI_EXTRA_REFERENCE_LIMIT,
resolveEditorProviderReferenceLimit(input.model) - 1,
),
'UI素材参考图',
);
return requestJson<EditorIconSpritesheetGenerationResponse>(
EDITOR_UI_DESIGN_ASSET_EXTRACTION_API,
jsonRequest('POST', {
sourceImageSrc: input.sourceImageSrc,
...(input.model ? { model: input.model } : {}),
...(input.screenColor ? { screenColor: input.screenColor } : {}),
...(input.segModel ? { segModel: input.segModel } : {}),
...(input.referenceImageSrcs?.length
? { referenceImageSrcs: input.referenceImageSrcs }
: {}),
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
...(input.spritesheetLabel
? { spritesheetLabel: input.spritesheetLabel }
: {}),
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
...(input.imageSize ? { imageSize: input.imageSize } : {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),
}),
'提取UI设计图素材失败',
{
timeoutMs: 1_200_000,
retry: EDITOR_GENERATION_REQUEST_RETRY_OPTIONS,
},
);
}
export async function editEditorImage(input: EditorImageEditInput) {
assertStableEditorMediaReferences(input.referenceImageSrcs, '修改参考图');
assertEditorReferenceLimit(
input.referenceImageSrcs,
Math.min(
EDITOR_IMAGE_EDIT_EXTRA_REFERENCE_LIMIT,
resolveEditorProviderReferenceLimit(input.model) - 1,
),
'修改参考图',
);
return requestJson<EditorImageGenerationResponse>(
EDITOR_IMAGE_EDIT_API,
jsonRequest('POST', {
prompt: input.prompt,
sourceReferenceId: input.sourceReferenceId,
...(input.size ? { size: input.size } : {}),
...(input.model ? { model: input.model } : {}),
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
...(input.imageSize ? { imageSize: input.imageSize } : {}),
...(input.referenceImageSrcs?.length
? { referenceImageSrcs: input.referenceImageSrcs }
: {}),
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
...(input.targetLayerId ? { targetLayerId: input.targetLayerId } : {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),
}),
'修改图片失败',
{
timeoutMs: 1_200_000,
retry: EDITOR_GENERATION_REQUEST_RETRY_OPTIONS,
},
);
}
export async function removeEditorImageBackground(
input: EditorBackgroundRemovalInput,
) {
assertStableEditorMediaReference(input.sourceImageSrc, '待去除背景图片');
return requestJson<EditorBackgroundRemovalResponse>(
EDITOR_BACKGROUND_REMOVAL_API,
jsonRequest('POST', {
sourceImageSrc: input.sourceImageSrc,
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.targetLayerId ? { targetLayerId: input.targetLayerId } : {}),
...(input.assetKind ? { assetKind: input.assetKind } : {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
...(input.sourceResourceId
? { sourceResourceId: input.sourceResourceId }
: {}),
...(input.taskId ? { taskId: input.taskId } : {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),
}),
'去除背景失败',
{
timeoutMs: 1_200_000,
retry: EDITOR_GENERATION_REQUEST_RETRY_OPTIONS,
},
);
}
export async function snapEditorImageToPixelArt(
input: EditorPixelArtSnapInput,
) {
assertStableEditorMediaReference(input.sourceImageSrc, '待完美像素处理图片');
return requestJson<EditorPixelArtSnapResponse>(
EDITOR_PIXEL_ART_SNAP_API,
jsonRequest('POST', {
sourceImageSrc: input.sourceImageSrc,
projectId: input.projectId,
...(input.sourceResourceId
? { sourceResourceId: input.sourceResourceId }
: {}),
...(input.assetKind ? { assetKind: input.assetKind } : {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
canvasCompletion: input.canvasCompletion,
}),
'完美像素处理失败',
{
timeoutMs: 120_000,
},
);
}
export async function generateEditorCharacterAnimation(
input: EditorCharacterAnimationGenerationInput,
) {
assertStableEditorMediaReference(input.sourceImageSrc, '角色动画源图');
return requestJson<EditorCharacterAnimationGenerationResult>(
EDITOR_CHARACTER_ANIMATION_GENERATION_API,
jsonRequest('POST', {
...input,
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.sourceResourceId
? { sourceResourceId: input.sourceResourceId }
: {}),
}),
'生成角色动画失败',
{
timeoutMs: 1_200_000,
retry: EDITOR_GENERATION_REQUEST_RETRY_OPTIONS,
},
);
}
export async function generateEditorVideo(input: EditorVideoGenerationInput) {
assertStableEditorVideoReferences(input);
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,
webSearchEnabled: input.webSearchEnabled ?? true,
...(input.referenceImageSrcs?.length
? { referenceImageSrcs: input.referenceImageSrcs }
: {}),
...(input.referenceVideoSrcs?.length
? { referenceVideoSrcs: input.referenceVideoSrcs }
: {}),
...(input.referenceAudioSrcs?.length
? { referenceAudioSrcs: input.referenceAudioSrcs }
: {}),
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.sourceResourceId
? { sourceResourceId: input.sourceResourceId }
: {}),
...(input.assetKind ? { assetKind: input.assetKind } : {}),
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
}),
'生成视频失败',
{
timeoutMs: 1_200_000,
retry: EDITOR_GENERATION_REQUEST_RETRY_OPTIONS,
},
);
}
export async function generateEditorSoundEffect(
input: EditorSoundEffectGenerationInput,
) {
const duration = input.duration ?? null;
if (
duration !== null &&
(!Number.isFinite(duration) ||
duration < SOUND_EFFECT_DURATION_MIN_SECONDS ||
duration > SOUND_EFFECT_DURATION_MAX_SECONDS)
) {
throw new Error('游戏音效时长必须在 0.5-30 秒之间');
}
return requestJson<EditorAudioGenerationResponse>(
EDITOR_SOUND_EFFECT_GENERATION_API,
jsonRequest('POST', {
prompt: input.prompt,
model: input.model,
duration,
loop: input.loop ?? false,
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
}),
'生成游戏音效失败',
{
timeoutMs: 1_200_000,
},
);
}
function requestEditorBackgroundMusicPromptAssist(
path: string,
input: BackgroundMusicPromptAssistRequest,
fallbackMessage: string,
options: EditorBackgroundMusicPromptAssistOptions,
) {
return requestJson<BackgroundMusicPromptAssistResponse>(
path,
{
...jsonRequest('POST', {
currentPrompt: input.currentPrompt,
}),
signal: options.signal,
},
fallbackMessage,
{
timeoutMs: EDITOR_BACKGROUND_MUSIC_PROMPT_ASSIST_TIMEOUT_MS,
},
);
}
export function completeEditorBackgroundMusicPrompt(
input: BackgroundMusicPromptAssistRequest,
options: EditorBackgroundMusicPromptAssistOptions = {},
) {
return requestEditorBackgroundMusicPromptAssist(
EDITOR_BACKGROUND_MUSIC_PROMPT_COMPLETION_API,
input,
'AI 补全背景音乐提示词失败',
options,
);
}
export function simplifyEditorBackgroundMusicPrompt(
input: BackgroundMusicPromptAssistRequest,
options: EditorBackgroundMusicPromptAssistOptions = {},
) {
return requestEditorBackgroundMusicPromptAssist(
EDITOR_BACKGROUND_MUSIC_PROMPT_SIMPLIFICATION_API,
input,
'简化背景音乐提示词失败',
options,
);
}
export function optimizeEditorSoundEffectPrompt(
input: SoundEffectPromptOptimizeRequest,
options: EditorSoundEffectPromptOptimizeOptions = {},
) {
return requestJson<SoundEffectPromptOptimizeResponse>(
EDITOR_SOUND_EFFECT_PROMPT_OPTIMIZATION_API,
{
...jsonRequest('POST', {
currentPrompt: input.currentPrompt,
}),
signal: options.signal,
},
'优化游戏音效提示词失败',
{
timeoutMs: EDITOR_SOUND_EFFECT_PROMPT_OPTIMIZE_TIMEOUT_MS,
},
);
}
export type Model3dGenerationSubmissionResponse = {
operationId: string;
jobKind: string;
status: ExternalGenerationJobStatusRecord;
statusUrl: string;
};
/**
* 3D 生成提交:两个端点都要求 `Idempotency-Key`,键由调用方铸造并与当次请求内容绑定。
* 这里是画布链路里唯一发送该请求头的生成入口。
*/
export async function submitModel3dGenerationRequest({
endpoint,
body,
idempotencyKey,
}: {
endpoint: Model3dPricingEndpoint;
body: Record<string, unknown>;
idempotencyKey: string;
}) {
return requestJson<Model3dGenerationSubmissionResponse>(
endpoint === 'image-to-model'
? EDITOR_MODEL3D_IMAGE_TO_MODEL_API
: EDITOR_MODEL3D_TEXT_TO_MODEL_API,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(body),
},
'提交 3D 模型生成失败',
{
timeoutMs: EDITOR_MODEL3D_SUBMISSION_TIMEOUT_MS,
...EDITOR_GENERATION_REQUEST_RETRY_OPTIONS,
},
);
}
export async function generateEditorBackgroundMusic(
input: EditorBackgroundMusicGenerationInput,
) {
return requestJson<EditorAudioGenerationResponse>(
EDITOR_BACKGROUND_MUSIC_GENERATION_API,
jsonRequest('POST', {
gptDescriptionPrompt: input.gptDescriptionPrompt,
makeInstrumental: input.makeInstrumental,
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),
...(input.generationInputs
? { generationInputs: input.generationInputs }
: {}),
...(input.assetFolderId ? { assetFolderId: input.assetFolderId } : {}),
...(input.assetLabel ? { assetLabel: input.assetLabel } : {}),
}),
'生成游戏背景音乐失败',
{
timeoutMs: 1_200_000,
},
);
}