收紧编辑器媒体引用契约
禁止当前画布与外部编辑器接口接收 Data URL 和 Blob URL 生成请求统一先上传 OSS 并使用稳定资源引用 角色动画和视频签名前校验资源归属并支持资源与素材 ID 同步前后端 256KB 引用限制及相关文档测试
This commit is contained in:
@@ -411,7 +411,6 @@ export const SEEDANCE_VIDEO_REFERENCE_LIMITS = {
|
||||
image: 9,
|
||||
video: 3,
|
||||
audio: 3,
|
||||
requestBytes: 64 * 1024 * 1024,
|
||||
} as const;
|
||||
export const QUICK_EDIT_REFERENCE_LIMIT = 8;
|
||||
export const CHARACTER_ANIMATION_ACTION_PROMPTS = [
|
||||
|
||||
@@ -91,11 +91,6 @@ export function resolveImageReferenceSubmissionSource(reference: {
|
||||
return reference.objectKey?.trim() || reference.src;
|
||||
}
|
||||
|
||||
function estimateReferenceRequestBytes(references: Array<{ src: string }>) {
|
||||
// 中文注释:JSON 包装字段还有少量额外字节;这里按 src 字符串长度估算,前后端都会再兜底校验 64MB。
|
||||
return references.reduce((sum, reference) => sum + reference.src.length, 0);
|
||||
}
|
||||
|
||||
function buildSeedanceVideoReferenceInput(
|
||||
references: NonNullable<GenerateDialogState['generationReferences']>,
|
||||
) {
|
||||
@@ -129,13 +124,6 @@ function buildSeedanceVideoReferenceInput(
|
||||
) {
|
||||
throw new Error('参考音频必须搭配参考图片或参考视频');
|
||||
}
|
||||
if (
|
||||
estimateReferenceRequestBytes(references) >
|
||||
SEEDANCE_VIDEO_REFERENCE_LIMITS.requestBytes
|
||||
) {
|
||||
throw new Error('参考文件总请求体不能超过 64MB');
|
||||
}
|
||||
|
||||
return {
|
||||
referenceImageSrcs: imageReferences.map(resolveReferenceSource),
|
||||
referenceVideoSrcs: videoReferences.map(resolveReferenceSource),
|
||||
|
||||
@@ -537,6 +537,31 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(firstOptions.pathSegments).not.toEqual(secondOptions.pathSegments);
|
||||
});
|
||||
|
||||
it('uploads image references without an object reference before generation', async () => {
|
||||
resolveEditorImageReferenceDataUrlMock.mockResolvedValueOnce(
|
||||
'data:image/png;base64,ZXhhbXBsZQ==',
|
||||
);
|
||||
const result = await resolveEditorGenerationMediaReference(
|
||||
{ src: '/creation-type-references/example.webp' },
|
||||
'image',
|
||||
'project-1',
|
||||
);
|
||||
|
||||
expect(resolveEditorImageReferenceDataUrlMock).toHaveBeenCalledWith(
|
||||
'/creation-type-references/example.webp',
|
||||
);
|
||||
expect(uploadEditorMediaAssetFileMock).toHaveBeenCalledWith(
|
||||
expect.any(File),
|
||||
'image',
|
||||
expect.objectContaining({
|
||||
assetKind: 'editor_generation_reference_image',
|
||||
}),
|
||||
);
|
||||
expect(result).toBe(
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('submits quick edits and updates the source layer directly', async () => {
|
||||
editEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({
|
||||
|
||||
@@ -218,20 +218,27 @@ export async function resolveEditorGenerationMediaReference(
|
||||
projectId?: string | null,
|
||||
options: EditorGenerationMediaReferenceOptions = {},
|
||||
) {
|
||||
const resourceId = reference.resourceId?.trim();
|
||||
const hasRegisteredReference =
|
||||
options.allowRegisteredIds !== false &&
|
||||
((resourceId &&
|
||||
!resourceId.startsWith('local-resource-') &&
|
||||
!resourceId.startsWith('generation-dialog:')) ||
|
||||
reference.sourceAssetId?.trim());
|
||||
const source = resolveEditorGenerationMediaReferenceSource(
|
||||
reference,
|
||||
options.allowRegisteredIds !== false,
|
||||
);
|
||||
const inlineSource = /^(?:data|blob):/iu.test(source);
|
||||
const remoteImageSource =
|
||||
options.requireImageObjectReference === true &&
|
||||
const imageSourceRequiresUpload =
|
||||
options.requireImageObjectReference !== false &&
|
||||
mediaType === 'image' &&
|
||||
!reference.objectKey?.trim() &&
|
||||
/^(?:https?:|\/api\/)/iu.test(source);
|
||||
if (!inlineSource && !remoteImageSource) {
|
||||
!hasRegisteredReference;
|
||||
if (!inlineSource && !imageSourceRequiresUpload) {
|
||||
return source;
|
||||
}
|
||||
const uploadSource = remoteImageSource
|
||||
const uploadSource = imageSourceRequiresUpload && !inlineSource
|
||||
? await resolveEditorImageReferenceDataUrl(source)
|
||||
: source;
|
||||
return uploadEditorGenerationInlineMediaSource(
|
||||
|
||||
@@ -28,9 +28,8 @@ function encodeBytesAsBase64(bytes: Uint8Array) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片画布的生成编辑接口只接收图片 Data URL。
|
||||
* 画布里既有上传 / 生成图,也有内置 public 参考图和历史 generated 路径;
|
||||
* 提交给后端前统一读取成 Data URL,避免后端误收到普通 URL 后进入 Data URL 校验错误。
|
||||
* 将画布图片读取为浏览器临时 Data URL,供压缩、标注等本地处理使用。
|
||||
* 生成请求提交前必须再上传 OSS,并把 objectKey 传给后端;Data URL 不属于接口契约。
|
||||
*/
|
||||
export async function resolveEditorImageReferenceDataUrl(
|
||||
source: string,
|
||||
@@ -121,7 +120,7 @@ export async function compressEditorImageReferenceDataUrlForGeneration(
|
||||
return dataUrl;
|
||||
}
|
||||
|
||||
// 中文注释:参考图只用于模型理解视觉方向,提交前压缩可避免原图 Data URL 撑爆 JSON 请求体。
|
||||
// 中文注释:参考图只用于模型理解视觉方向,上传 OSS 前压缩可减少传输与存储体积。
|
||||
if (mimeType === 'image/jpeg') {
|
||||
context.fillStyle = '#fff';
|
||||
context.fillRect(0, 0, targetWidth, targetHeight);
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
generateEditorVideo,
|
||||
listEditorProjects,
|
||||
listPublicEditorProjectResources,
|
||||
loadEditorGenerationPricing,
|
||||
loadEditorAssetLibrary,
|
||||
loadEditorGenerationPricing,
|
||||
loadEditorProject,
|
||||
loadOrCreateRecentEditorProject,
|
||||
removeEditorImageBackground,
|
||||
@@ -774,6 +774,47 @@ describe('editorProjectClient', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects inline media before submitting editor generation requests', async () => {
|
||||
await expect(
|
||||
generateEditorImage({
|
||||
prompt: '一张画布图片',
|
||||
referenceImageSrcs: ['data:image/png;base64,inline'],
|
||||
}),
|
||||
).rejects.toThrow('必须先上传 OSS');
|
||||
|
||||
await expect(
|
||||
generateEditorVideo({
|
||||
prompt: '生成视频',
|
||||
model: 'seedance2.0-fast',
|
||||
aspectRatio: '16:9',
|
||||
durationSeconds: 5,
|
||||
resolution: '720p',
|
||||
mode: 'std',
|
||||
sound: 'off',
|
||||
referenceVideoSrcs: ['blob:local-video'],
|
||||
}),
|
||||
).rejects.toThrow('必须先上传 OSS');
|
||||
|
||||
expect(requestJsonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects oversized stable video reference fields before submission', async () => {
|
||||
await expect(
|
||||
generateEditorVideo({
|
||||
prompt: '生成视频',
|
||||
model: 'seedance2.0-fast',
|
||||
aspectRatio: '16:9',
|
||||
durationSeconds: 5,
|
||||
resolution: '720p',
|
||||
mode: 'std',
|
||||
sound: 'off',
|
||||
referenceVideoSrcs: [`https://assets.example.test/${'a'.repeat(256 * 1024)}`],
|
||||
}),
|
||||
).rejects.toThrow('稳定引用字段总长度不能超过 256KB');
|
||||
|
||||
expect(requestJsonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes image model options to editor image generation', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
imageSrc: 'data:image/png;base64,character',
|
||||
@@ -919,8 +960,8 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
const result = await generateEditorIconSpritesheet({
|
||||
referenceImageSrc: 'data:image/png;base64,spec',
|
||||
referenceImageSrcs: ['data:image/png;base64,icon-ref'],
|
||||
referenceImageSrc: '/generated-images/editor/spec.png',
|
||||
referenceImageSrcs: ['/generated-images/editor/icon-ref.png'],
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
assetLabel: '冒险游戏图标',
|
||||
});
|
||||
@@ -936,8 +977,8 @@ describe('editorProjectClient', () => {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
referenceImageSrc: 'data:image/png;base64,spec',
|
||||
referenceImageSrcs: ['data:image/png;base64,icon-ref'],
|
||||
referenceImageSrc: '/generated-images/editor/spec.png',
|
||||
referenceImageSrcs: ['/generated-images/editor/icon-ref.png'],
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
assetLabel: '冒险游戏图标',
|
||||
@@ -965,7 +1006,7 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
await generateEditorIconSpritesheet({
|
||||
referenceImageSrc: 'data:image/png;base64,spec',
|
||||
referenceImageSrc: '/generated-images/editor/spec.png',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
@@ -986,7 +1027,7 @@ describe('editorProjectClient', () => {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
referenceImageSrc: 'data:image/png;base64,spec',
|
||||
referenceImageSrc: '/generated-images/editor/spec.png',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
@@ -1105,11 +1146,11 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
const result = await extractEditorUiDesignAssets({
|
||||
sourceImageSrc: 'data:image/png;base64,ui-design',
|
||||
sourceImageSrc: '/generated-images/editor/ui-design.png',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#7FB3FF',
|
||||
segModel: 'anime-seg',
|
||||
referenceImageSrcs: ['data:image/png;base64,ui-ref'],
|
||||
referenceImageSrcs: ['/generated-images/editor/ui-ref.png'],
|
||||
projectId: 'editor-project-1',
|
||||
assetFolderId: 'project',
|
||||
spritesheetLabel: '战斗UI设计图 素材图集',
|
||||
@@ -1128,11 +1169,11 @@ describe('editorProjectClient', () => {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sourceImageSrc: 'data:image/png;base64,ui-design',
|
||||
sourceImageSrc: '/generated-images/editor/ui-design.png',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#7FB3FF',
|
||||
segModel: 'anime-seg',
|
||||
referenceImageSrcs: ['data:image/png;base64,ui-ref'],
|
||||
referenceImageSrcs: ['/generated-images/editor/ui-ref.png'],
|
||||
projectId: 'editor-project-1',
|
||||
generationInputs: {
|
||||
fields: [{ title: '来源', value: '战斗UI设计图' }],
|
||||
@@ -1212,7 +1253,7 @@ describe('editorProjectClient', () => {
|
||||
kind: 'publication-material',
|
||||
aspectRatio: '9:16',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
referenceImageSrcs: ['/generated-images/editor/publication-ref.png'],
|
||||
});
|
||||
|
||||
expect(result.height).toBe(1280);
|
||||
@@ -1227,7 +1268,7 @@ describe('editorProjectClient', () => {
|
||||
kind: 'publication-material',
|
||||
aspectRatio: '9:16',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
referenceImageSrcs: ['/generated-images/editor/publication-ref.png'],
|
||||
}),
|
||||
}),
|
||||
'生成图片失败',
|
||||
@@ -1259,7 +1300,7 @@ describe('editorProjectClient', () => {
|
||||
|
||||
const result = await generateEditorCharacterAnimation({
|
||||
sourceLayerId: 'layer-character',
|
||||
sourceImageSrc: 'data:image/png;base64,character',
|
||||
sourceImageSrc: '/generated-images/editor/character.png',
|
||||
sourceWidth: 1024,
|
||||
sourceHeight: 1024,
|
||||
promptText: '待机',
|
||||
@@ -1279,7 +1320,7 @@ describe('editorProjectClient', () => {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sourceLayerId: 'layer-character',
|
||||
sourceImageSrc: 'data:image/png;base64,character',
|
||||
sourceImageSrc: '/generated-images/editor/character.png',
|
||||
sourceWidth: 1024,
|
||||
sourceHeight: 1024,
|
||||
promptText: '待机',
|
||||
@@ -1395,7 +1436,7 @@ describe('editorProjectClient', () => {
|
||||
resolution: '720p',
|
||||
mode: 'std',
|
||||
sound: 'off',
|
||||
referenceImageSrcs: ['data:image/png;base64,image'],
|
||||
referenceImageSrcs: ['/generated-images/editor/video-reference.png'],
|
||||
referenceVideoSrcs: [
|
||||
'generated-character-drafts/editor/seedance-references/video/video.mp4',
|
||||
],
|
||||
@@ -1418,7 +1459,7 @@ describe('editorProjectClient', () => {
|
||||
mode: 'std',
|
||||
sound: 'off',
|
||||
webSearchEnabled: true,
|
||||
referenceImageSrcs: ['data:image/png;base64,image'],
|
||||
referenceImageSrcs: ['/generated-images/editor/video-reference.png'],
|
||||
referenceVideoSrcs: [
|
||||
'generated-character-drafts/editor/seedance-references/video/video.mp4',
|
||||
],
|
||||
@@ -1578,12 +1619,12 @@ describe('editorProjectClient', () => {
|
||||
|
||||
const result = await editEditorImage({
|
||||
prompt: '把画面改成黄昏光线',
|
||||
sourceImageSrc: 'data:image/png;base64,source',
|
||||
sourceImageSrc: '/generated-images/editor/source.png',
|
||||
size: '1024x1024',
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: ['data:image/png;base64,style'],
|
||||
referenceImageSrcs: ['/generated-images/editor/style.png'],
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
assetFolderId: 'project',
|
||||
@@ -1604,12 +1645,12 @@ describe('editorProjectClient', () => {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt: '把画面改成黄昏光线',
|
||||
sourceImageSrc: 'data:image/png;base64,source',
|
||||
sourceImageSrc: '/generated-images/editor/source.png',
|
||||
size: '1024x1024',
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: ['data:image/png;base64,style'],
|
||||
referenceImageSrcs: ['/generated-images/editor/style.png'],
|
||||
projectId: 'editor-project-1',
|
||||
assetKind: 'character',
|
||||
generationInputs: {
|
||||
@@ -1643,7 +1684,7 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
const response = await removeEditorImageBackground({
|
||||
sourceImageSrc: 'data:image/png;base64,source',
|
||||
sourceImageSrc: '/generated-images/editor/source.png',
|
||||
projectId: 'editor-project-1',
|
||||
targetLayerId: 'layer-source',
|
||||
canvasCompletion: {
|
||||
@@ -1668,7 +1709,7 @@ describe('editorProjectClient', () => {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sourceImageSrc: 'data:image/png;base64,source',
|
||||
sourceImageSrc: '/generated-images/editor/source.png',
|
||||
projectId: 'editor-project-1',
|
||||
targetLayerId: 'layer-source',
|
||||
canvasCompletion: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { requestJson } from '../apiClient';
|
||||
import type { ExternalGenerationJobStatusRecord } from '../../../packages/shared/src/contracts/externalGeneration';
|
||||
import { requestJson } from '../apiClient';
|
||||
import { EDITOR_REQUEST_RETRY_OPTIONS } from './editorRetryOptions';
|
||||
|
||||
const EDITOR_PROJECT_API_BASE = '/api/editor/projects';
|
||||
@@ -25,8 +25,38 @@ const EDITOR_BACKGROUND_MUSIC_GENERATION_API =
|
||||
'/api/editor/audios/background-music/generations';
|
||||
const EDITOR_GENERATION_PRICING_API = '/api/editor/generation-pricing';
|
||||
const EDITOR_IMAGE_MODEL_NANOBANANA2 = 'gemini-3.1-flash-image-preview';
|
||||
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 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;
|
||||
@@ -874,6 +904,7 @@ export async function toggleEditorShowcaseAssetLike(
|
||||
}
|
||||
|
||||
export async function generateEditorImage(input: EditorImageGenerationInput) {
|
||||
assertStableEditorMediaReferences(input.referenceImageSrcs, '生成参考图');
|
||||
return requestJson<EditorImageGenerationResponse>(
|
||||
EDITOR_IMAGE_GENERATION_API,
|
||||
jsonRequest('POST', {
|
||||
@@ -913,6 +944,11 @@ export async function generateEditorImage(input: EditorImageGenerationInput) {
|
||||
export async function generateEditorIconSpritesheet(
|
||||
input: EditorIconSpritesheetGenerationInput,
|
||||
) {
|
||||
assertStableEditorMediaReference(input.referenceImageSrc, '图标素材规范');
|
||||
assertStableEditorMediaReferences(
|
||||
input.referenceImageSrcs,
|
||||
'图标素材参考图',
|
||||
);
|
||||
return requestJson<EditorIconSpritesheetGenerationResponse>(
|
||||
EDITOR_ICON_SPRITESHEET_GENERATION_API,
|
||||
jsonRequest('POST', {
|
||||
@@ -962,6 +998,8 @@ export async function splitEditorIconSpritesheet(
|
||||
export async function extractEditorUiDesignAssets(
|
||||
input: EditorUiDesignAssetExtractionInput,
|
||||
) {
|
||||
assertStableEditorMediaReference(input.sourceImageSrc, 'UI设计图');
|
||||
assertStableEditorMediaReferences(input.referenceImageSrcs, 'UI素材参考图');
|
||||
return requestJson<EditorIconSpritesheetGenerationResponse>(
|
||||
EDITOR_UI_DESIGN_ASSET_EXTRACTION_API,
|
||||
jsonRequest('POST', {
|
||||
@@ -995,6 +1033,8 @@ export async function extractEditorUiDesignAssets(
|
||||
}
|
||||
|
||||
export async function editEditorImage(input: EditorImageEditInput) {
|
||||
assertStableEditorMediaReference(input.sourceImageSrc, '待修改图片');
|
||||
assertStableEditorMediaReferences(input.referenceImageSrcs, '修改参考图');
|
||||
return requestJson<EditorImageGenerationResponse>(
|
||||
EDITOR_IMAGE_EDIT_API,
|
||||
jsonRequest('POST', {
|
||||
@@ -1033,6 +1073,7 @@ export async function editEditorImage(input: EditorImageEditInput) {
|
||||
export async function removeEditorImageBackground(
|
||||
input: EditorBackgroundRemovalInput,
|
||||
) {
|
||||
assertStableEditorMediaReference(input.sourceImageSrc, '待去除背景图片');
|
||||
return requestJson<EditorBackgroundRemovalResponse>(
|
||||
EDITOR_BACKGROUND_REMOVAL_API,
|
||||
jsonRequest('POST', {
|
||||
@@ -1064,6 +1105,7 @@ export async function removeEditorImageBackground(
|
||||
export async function generateEditorCharacterAnimation(
|
||||
input: EditorCharacterAnimationGenerationInput,
|
||||
) {
|
||||
assertStableEditorMediaReference(input.sourceImageSrc, '角色动画源图');
|
||||
return requestJson<EditorCharacterAnimationGenerationResult>(
|
||||
EDITOR_CHARACTER_ANIMATION_GENERATION_API,
|
||||
jsonRequest('POST', {
|
||||
@@ -1088,6 +1130,7 @@ export async function generateEditorCharacterAnimation(
|
||||
}
|
||||
|
||||
export async function generateEditorVideo(input: EditorVideoGenerationInput) {
|
||||
assertStableEditorVideoReferences(input);
|
||||
return requestJson<EditorVideoGenerationResponse>(
|
||||
EDITOR_VIDEO_GENERATION_API,
|
||||
jsonRequest('POST', {
|
||||
|
||||
Reference in New Issue
Block a user