修复外部生成任务大载荷导致的内存膨胀
阻止图片编辑器将 Data URL 和超限 JSON 写入生成任务,复用 objectKey 等轻量引用 新增外部生成任务摘要投影与分批 payload 压缩回填流程,避免正式列表读取大字段 收紧 SpacetimeDB 发布为永不删数据并移除 Jenkins 普通清库入口,补齐维护窗口门禁 更新生成绑定、运维文档和回归测试,并完成生产数据副本迁移与压缩验证
This commit is contained in:
@@ -25,6 +25,7 @@ import type { UiAssetExtractionState } from './ImageCanvasUiAssetExtractionModel
|
||||
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
|
||||
import {
|
||||
applyQueuedEditorGenerationProject,
|
||||
resolveEditorGenerationMediaReference,
|
||||
useImageCanvasGenerationSubmissionWorkflow,
|
||||
} from './useImageCanvasGenerationSubmissionWorkflow';
|
||||
|
||||
@@ -32,6 +33,7 @@ const resolveEditorImageReferenceDataUrlMock = vi.hoisted(() => vi.fn());
|
||||
const resolveEditorImageReferenceDataUrlForGenerationMock = vi.hoisted(() =>
|
||||
vi.fn(),
|
||||
);
|
||||
const uploadEditorMediaAssetFileMock = vi.hoisted(() => vi.fn());
|
||||
const editEditorImageMock = vi.hoisted(() => vi.fn());
|
||||
const extractEditorUiDesignAssetsMock = vi.hoisted(() => vi.fn());
|
||||
const renderUiDesignAssetExtractionMarkedImageMock = vi.hoisted(() => vi.fn());
|
||||
@@ -68,6 +70,10 @@ vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../services/image-editor/editorMediaAssetUploadClient', () => ({
|
||||
uploadEditorMediaAssetFile: uploadEditorMediaAssetFileMock,
|
||||
}));
|
||||
|
||||
vi.mock('../../services/external-generation', () => ({
|
||||
getExternalGenerationJobStatus: getExternalGenerationJobStatusMock,
|
||||
}));
|
||||
@@ -466,6 +472,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
beforeEach(() => {
|
||||
resolveEditorImageReferenceDataUrlMock.mockReset();
|
||||
resolveEditorImageReferenceDataUrlForGenerationMock.mockReset();
|
||||
uploadEditorMediaAssetFileMock.mockReset();
|
||||
editEditorImageMock.mockReset();
|
||||
extractEditorUiDesignAssetsMock.mockReset();
|
||||
renderUiDesignAssetExtractionMarkedImageMock.mockReset();
|
||||
@@ -483,11 +490,47 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
resolveEditorImageReferenceDataUrlForGenerationMock.mockImplementation(
|
||||
async (src: string) => src,
|
||||
);
|
||||
uploadEditorMediaAssetFileMock.mockResolvedValue({
|
||||
src: 'https://signed.example.test/generation-reference.png',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
assetObjectId: 'asset-object-generation-reference',
|
||||
legacyPublicPath:
|
||||
'/generated-character-drafts/editor/generation-references/reference.png',
|
||||
});
|
||||
renderUiDesignAssetExtractionMarkedImageMock.mockResolvedValue(
|
||||
'data:image/png;base64,marked-ui-design',
|
||||
'data:image/png;base64,bWFya2VkLXVpLWRlc2lnbg==',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses distinct upload paths for concurrent inline references in the same millisecond', async () => {
|
||||
const dateNowSpy = vi.spyOn(Date, 'now').mockReturnValue(1_752_144_000_000);
|
||||
try {
|
||||
await Promise.all([
|
||||
resolveEditorGenerationMediaReference(
|
||||
{ src: 'data:image/png;base64,YQ==' },
|
||||
'image',
|
||||
'project-1',
|
||||
),
|
||||
resolveEditorGenerationMediaReference(
|
||||
{ src: 'data:image/png;base64,Yg==' },
|
||||
'image',
|
||||
'project-1',
|
||||
),
|
||||
]);
|
||||
} finally {
|
||||
dateNowSpy.mockRestore();
|
||||
}
|
||||
|
||||
expect(uploadEditorMediaAssetFileMock).toHaveBeenCalledTimes(2);
|
||||
const [firstFile, , firstOptions] =
|
||||
uploadEditorMediaAssetFileMock.mock.calls[0] ?? [];
|
||||
const [secondFile, , secondOptions] =
|
||||
uploadEditorMediaAssetFileMock.mock.calls[1] ?? [];
|
||||
expect((firstFile as File).name).not.toBe((secondFile as File).name);
|
||||
expect(firstOptions.pathSegments).not.toEqual(secondOptions.pathSegments);
|
||||
});
|
||||
|
||||
it('submits quick edits and updates the source layer directly', async () => {
|
||||
editEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: '快速修图' }),
|
||||
@@ -506,7 +549,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '快速修图',
|
||||
sourceImageSrc: 'data:image/png;base64,source.png',
|
||||
sourceImageSrc: 'resource-source',
|
||||
size: '1024x768',
|
||||
model: 'gpt-image-2',
|
||||
}),
|
||||
@@ -516,6 +559,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
'canvasCompletion',
|
||||
);
|
||||
expect(generateEditorImageMock).not.toHaveBeenCalled();
|
||||
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-source:源图:resource-source:-:-:data:image/png;base64,generated',
|
||||
@@ -606,7 +650,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '把当前图改成雨天',
|
||||
sourceImageSrc: 'data:image/png;base64,source.png',
|
||||
sourceImageSrc: 'resource-source',
|
||||
size: '1024x768',
|
||||
model: 'gpt-image-2',
|
||||
sourceResourceId: 'resource-source',
|
||||
@@ -727,7 +771,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
/>,
|
||||
);
|
||||
renderUiDesignAssetExtractionMarkedImageMock.mockResolvedValueOnce(
|
||||
'data:image/png;base64,marked-source',
|
||||
'data:image/png;base64,bWFya2VkLXNvdXJjZQ==',
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
|
||||
@@ -751,13 +795,24 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(uploadEditorMediaAssetFileMock).toHaveBeenCalledWith(
|
||||
expect.any(File),
|
||||
'image',
|
||||
expect.objectContaining({
|
||||
assetKind: 'editor_generation_reference_image',
|
||||
}),
|
||||
);
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceImageSrc: 'data:image/png;base64,marked-source',
|
||||
sourceImageSrc:
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
targetLayerId: 'layer-source',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(
|
||||
uploadEditorMediaAssetFileMock.mock.invocationCallOrder[0],
|
||||
).toBeLessThan(editEditorImageMock.mock.invocationCallOrder[0] ?? 0);
|
||||
expect(editEditorImageMock.mock.calls[0]?.[0]).not.toHaveProperty(
|
||||
'referenceImageSrcs',
|
||||
);
|
||||
@@ -812,7 +867,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
/>,
|
||||
);
|
||||
renderUiDesignAssetExtractionMarkedImageMock.mockResolvedValueOnce(
|
||||
'data:image/png;base64,marked-source',
|
||||
'data:image/png;base64,bWFya2VkLXNvdXJjZQ==',
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
|
||||
@@ -839,7 +894,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
await waitFor(() => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceImageSrc: 'data:image/png;base64,marked-source',
|
||||
sourceImageSrc:
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
model: 'gpt-image-2',
|
||||
targetLayerId: 'layer-source',
|
||||
}),
|
||||
@@ -907,7 +963,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
imageSize: '1K',
|
||||
kind: 'character',
|
||||
model: 'gpt-image-2',
|
||||
referenceImageSrcs: ['data:image/png;base64,source.png'],
|
||||
referenceImageSrcs: ['resource-source'],
|
||||
assetKind: 'character',
|
||||
}),
|
||||
);
|
||||
@@ -959,8 +1015,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
title: '源视频',
|
||||
src: 'https://assets.example.test/source.mp4',
|
||||
objectKey: 'generated-character-drafts/editor/videos/source.mp4',
|
||||
src: 'data:video/mp4;base64,c291cmNlLXZpZGVv',
|
||||
mediaType: 'video',
|
||||
assetKind: 'video',
|
||||
model: 'gpt-image-2',
|
||||
@@ -1001,7 +1056,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
mode: 'std',
|
||||
sound: 'off',
|
||||
referenceVideoSrcs: [
|
||||
'generated-character-drafts/editor/videos/source.mp4',
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
],
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '源视频 快速编辑',
|
||||
@@ -1026,6 +1081,66 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uploads inline video references and keeps image objectKey and asset URLs', async () => {
|
||||
generateEditorVideoMock.mockResolvedValueOnce(createVideoGenerated());
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialDialog={{
|
||||
id: 'dialog-video-references',
|
||||
mode: 'video',
|
||||
prompt: '让画面动起来',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
videoModel: 'seedance2.0-fast',
|
||||
generationReferences: [
|
||||
{
|
||||
id: 'image-reference',
|
||||
label: '图片参考',
|
||||
src: 'data:image/png;base64,aW1hZ2U=',
|
||||
objectKey: 'generated-character-drafts/editor/refs/image.png',
|
||||
},
|
||||
{
|
||||
id: 'video-reference',
|
||||
label: '视频参考',
|
||||
src: 'data:video/mp4;base64,dmlkZW8=',
|
||||
mediaType: 'video',
|
||||
},
|
||||
{
|
||||
id: 'audio-reference',
|
||||
label: '音频参考',
|
||||
src: 'asset://asset-audio-reference',
|
||||
mediaType: 'audio',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(uploadEditorMediaAssetFileMock).toHaveBeenCalledWith(
|
||||
expect.any(File),
|
||||
'video',
|
||||
expect.objectContaining({
|
||||
assetKind: 'editor_generation_reference_video',
|
||||
}),
|
||||
);
|
||||
expect(generateEditorVideoMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
referenceImageSrcs: [
|
||||
'generated-character-drafts/editor/refs/image.png',
|
||||
],
|
||||
referenceVideoSrcs: [
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
],
|
||||
referenceAudioSrcs: ['asset://asset-audio-reference'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('submits character action quick edits through image edits', async () => {
|
||||
editEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({
|
||||
@@ -1075,7 +1190,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '让当前图跳得更高',
|
||||
sourceImageSrc: 'data:image/png;base64,frame.png',
|
||||
sourceImageSrc: 'resource-source',
|
||||
model: 'gpt-image-2',
|
||||
assetLabel: '角色动作 快速编辑',
|
||||
targetLayerId: 'layer-source',
|
||||
@@ -1117,7 +1232,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '修改当前图片',
|
||||
sourceImageSrc: 'data:image/png;base64,source.png',
|
||||
sourceImageSrc: 'resource-source',
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1341,10 +1456,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves uploaded objectKey references before submitting image generation', async () => {
|
||||
resolveEditorImageReferenceDataUrlForGenerationMock.mockResolvedValueOnce(
|
||||
'data:image/jpeg;base64,normalized-ref',
|
||||
);
|
||||
it('submits uploaded objectKey references without downloading or inlining them', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: '参考图生成' }),
|
||||
);
|
||||
@@ -1382,16 +1494,17 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
resolveEditorImageReferenceDataUrlForGenerationMock,
|
||||
).toHaveBeenCalledWith('generated-character-drafts/editor/refs/ref.png');
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '参考图生成',
|
||||
referenceImageSrcs: ['data:image/jpeg;base64,normalized-ref'],
|
||||
referenceImageSrcs: [
|
||||
'generated-character-drafts/editor/refs/ref.png',
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
|
||||
expect(uploadEditorMediaAssetFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refreshes the wallet balance after a queued generation reaches a terminal state', async () => {
|
||||
@@ -1525,10 +1638,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
1,
|
||||
unresolvedProject,
|
||||
);
|
||||
expect(applyProjectSnapshot).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
completedProject,
|
||||
);
|
||||
expect(applyProjectSnapshot).toHaveBeenNthCalledWith(2, completedProject);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
@@ -1691,7 +1801,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
{
|
||||
id: 'ref-1',
|
||||
label: '主视觉参考',
|
||||
src: 'data:image/png;base64,large-ref',
|
||||
src: 'DaTa:image/png;base64,bGFyZ2UtcmVm',
|
||||
},
|
||||
],
|
||||
placeholder: {
|
||||
@@ -1721,7 +1831,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '9:16',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: ['data:image/png;base64,large-ref'],
|
||||
referenceImageSrcs: [
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1742,10 +1854,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('compresses publication references before submitting generation', async () => {
|
||||
resolveEditorImageReferenceDataUrlForGenerationMock.mockResolvedValueOnce(
|
||||
'data:image/jpeg;base64,compressed-ref',
|
||||
);
|
||||
it('submits publication resource ids without downloading or inlining them', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({
|
||||
imageSrc: 'data:image/png;base64,publication',
|
||||
@@ -1773,6 +1882,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
id: 'ref-1',
|
||||
label: '主视觉参考',
|
||||
src: 'data:image/png;base64,large-original-ref',
|
||||
resourceId: 'resource-publication-ref',
|
||||
},
|
||||
],
|
||||
}}
|
||||
@@ -1783,18 +1893,17 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
resolveEditorImageReferenceDataUrlForGenerationMock,
|
||||
).toHaveBeenCalledWith('data:image/png;base64,large-original-ref');
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: 'publication-material',
|
||||
aspectRatio: '4:3',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: ['data:image/jpeg;base64,compressed-ref'],
|
||||
referenceImageSrcs: ['resource-publication-ref'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
|
||||
expect(uploadEditorMediaAssetFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('submits icon spec objects without requiring an icon spec reference', async () => {
|
||||
@@ -1913,7 +2022,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
iconSpecReference: {
|
||||
id: 'spec',
|
||||
label: '图标规范',
|
||||
src: 'data:image/png;base64,spec',
|
||||
src: 'data:image/png;base64,c3BlYw==',
|
||||
},
|
||||
iconDescriptions: ['旧返回', '旧设置'],
|
||||
placeholder: {
|
||||
@@ -1934,7 +2043,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
await waitFor(() => {
|
||||
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
referenceImageSrc: 'data:image/png;base64,spec',
|
||||
referenceImageSrc:
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
model: 'gpt-image-2',
|
||||
}),
|
||||
@@ -1992,7 +2102,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
id: 'ref-style',
|
||||
label: '风格参考',
|
||||
src: 'https://signed.example.test/icon-style.png?expires=1',
|
||||
objectKey: 'generated-character-drafts/editor/refs/icon-style.png',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/refs/icon-style.png',
|
||||
assetObjectId: 'assetobj-icon-style',
|
||||
},
|
||||
],
|
||||
@@ -2004,7 +2115,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交图标' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resolveEditorImageReferenceDataUrlForGenerationMock).not.toHaveBeenCalled();
|
||||
expect(
|
||||
resolveEditorImageReferenceDataUrlForGenerationMock,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
referenceImageSrc:
|
||||
@@ -2115,6 +2228,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
});
|
||||
|
||||
it('renders selected UI marks into the extraction reference image and sends derived extraction pricing', async () => {
|
||||
renderUiDesignAssetExtractionMarkedImageMock.mockResolvedValueOnce(
|
||||
'data:image/png;base64,bWFya2VkLXVpLWRlc2lnbg==',
|
||||
);
|
||||
extractEditorUiDesignAssetsMock.mockResolvedValueOnce({
|
||||
spritesheetImageSrc: 'data:image/png;base64,ui-sheet',
|
||||
spritesheetWidth: 512,
|
||||
@@ -2166,7 +2282,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
await waitFor(() => {
|
||||
expect(extractEditorUiDesignAssetsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceImageSrc: 'data:image/png;base64,marked-ui-design',
|
||||
sourceImageSrc:
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
screenColor: 'auto',
|
||||
segModel: 'birefnet',
|
||||
@@ -2213,7 +2330,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '提取带参考图UI素材' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resolveEditorImageReferenceDataUrlForGenerationMock).not.toHaveBeenCalled();
|
||||
expect(
|
||||
resolveEditorImageReferenceDataUrlForGenerationMock,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(extractEditorUiDesignAssetsMock).toHaveBeenCalledWith(
|
||||
@@ -2290,7 +2409,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
createLayer({
|
||||
id: 'character-layer',
|
||||
assetKind: 'character',
|
||||
objectKey: 'generated/character.png',
|
||||
src: 'data:image/png;base64,Y2hhcmFjdGVy',
|
||||
}),
|
||||
]}
|
||||
initialCharacterAnimationPanel={{
|
||||
@@ -2314,7 +2433,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(generateEditorCharacterAnimationMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceLayerId: 'character-layer',
|
||||
sourceImageSrc: 'generated/character.png',
|
||||
sourceImageSrc:
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
promptText: '跑步',
|
||||
model: 'seedance2.0-fast',
|
||||
}),
|
||||
@@ -2450,7 +2570,12 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialLayers={[createLayer({ id: 'character-layer' })]}
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
id: 'character-layer',
|
||||
objectKey: 'generated/character.png',
|
||||
}),
|
||||
]}
|
||||
initialCharacterAnimationPanel={{
|
||||
sourceLayerId: 'character-layer',
|
||||
promptText: '跑步',
|
||||
@@ -2478,7 +2603,12 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
generateEditorCharacterAnimationMock.mockRejectedValueOnce(new Error(''));
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialLayers={[createLayer({ id: 'character-layer' })]}
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
id: 'character-layer',
|
||||
objectKey: 'generated/character.png',
|
||||
}),
|
||||
]}
|
||||
initialCharacterAnimationPanel={{
|
||||
sourceLayerId: 'character-layer',
|
||||
promptText: '',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,12 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
@@ -12,9 +18,9 @@ import type {
|
||||
ImageContextMenuState,
|
||||
SidebarPanel,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { IMAGE_MODEL_GPT_IMAGE_2 } from './ImageCanvasGenerationModel';
|
||||
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
|
||||
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
|
||||
import { IMAGE_MODEL_GPT_IMAGE_2 } from './ImageCanvasGenerationModel';
|
||||
|
||||
const generateEditorImageMock = vi.hoisted(() => vi.fn());
|
||||
const generateEditorCharacterAnimationMock = vi.hoisted(() => vi.fn());
|
||||
@@ -1268,7 +1274,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
assetFolderId: undefined,
|
||||
assetKind: undefined,
|
||||
assetLabel: '生成图片 1',
|
||||
referenceImageSrcs: ['data:image/png;base64,source'],
|
||||
referenceImageSrcs: ['resource-source'],
|
||||
generationInputs: {
|
||||
fields: [{ title: '生成提示词', value: '一张生成图' }],
|
||||
references: [
|
||||
@@ -1514,6 +1520,14 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
});
|
||||
|
||||
it('submits spec generation with reference image and reference prompt semantics', async () => {
|
||||
uploadEditorMediaAssetFileMock.mockResolvedValueOnce({
|
||||
src: 'https://signed.example.test/generation-reference.png',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
assetObjectId: 'asset-object-generation-reference',
|
||||
legacyPublicPath:
|
||||
'/generated-character-drafts/editor/generation-references/reference.png',
|
||||
});
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: 'UI规范图' }),
|
||||
);
|
||||
@@ -1531,7 +1545,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
referenceImageSrcs: [
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
],
|
||||
prompt: expect.stringContaining('参考图生成规范'),
|
||||
}),
|
||||
);
|
||||
@@ -1630,7 +1646,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
expect(editEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '快速修图',
|
||||
sourceImageSrc: 'data:image/png;base64,source',
|
||||
sourceImageSrc: 'resource-source',
|
||||
assetLabel: '源图 快速编辑',
|
||||
sourceResourceId: 'resource-source',
|
||||
generationInputs: {
|
||||
@@ -1789,7 +1805,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(removeImageBackgroundMock).toHaveBeenCalledWith({
|
||||
sourceImageSrc: '/generated-character-drafts/editor/private.png',
|
||||
sourceImageSrc: 'generated-character-drafts/editor/private.png',
|
||||
projectId: undefined,
|
||||
targetLayerId: 'layer-source',
|
||||
assetKind: 'character',
|
||||
@@ -1832,6 +1848,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
);
|
||||
expect(removeImageBackgroundMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceImageSrc: 'resource-source',
|
||||
projectId: 'project-1',
|
||||
targetLayerId: 'layer-source',
|
||||
assetLabel: '源图 去背景',
|
||||
@@ -1845,6 +1862,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
resolveBackgroundRemoval?.({
|
||||
imageSrc:
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type EditorProjectResourceSnapshot,
|
||||
type EditorProjectSnapshot,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
@@ -36,7 +37,6 @@ import type {
|
||||
SpecFormValues,
|
||||
SpecGenerationType,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel';
|
||||
import {
|
||||
appendCharacterReference,
|
||||
appendGenerationReference,
|
||||
@@ -103,6 +103,8 @@ import {
|
||||
} from './ImageCanvasUiAssetExtractionModel';
|
||||
import {
|
||||
applyQueuedEditorGenerationProject,
|
||||
createEditorGenerationMediaUploadId,
|
||||
resolveEditorGenerationMediaReference,
|
||||
useImageCanvasGenerationSubmissionWorkflow,
|
||||
} from './useImageCanvasGenerationSubmissionWorkflow';
|
||||
|
||||
@@ -222,7 +224,10 @@ function preserveSourceLayerInProjectSnapshot(
|
||||
resources: sourceResource
|
||||
? [...project.resources, sourceResource]
|
||||
: project.resources,
|
||||
layers: [createProjectLayerSnapshotFromLayer(sourceLayer), ...project.layers],
|
||||
layers: [
|
||||
createProjectLayerSnapshotFromLayer(sourceLayer),
|
||||
...project.layers,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1434,9 +1439,10 @@ export function useImageCanvasGenerationWorkflow({
|
||||
cropExpandSourceLayer.resourceId;
|
||||
let cropExpandAssetKind = cropExpandSourceLayer.assetKind;
|
||||
if (cropExpandProjectId) {
|
||||
const cropExpandUploadId = createEditorGenerationMediaUploadId();
|
||||
const cropExpandFile = dataUrlToImageFile(
|
||||
result.imageSrc,
|
||||
`crop-expand-${cropExpandIndex}.png`,
|
||||
`crop-expand-${cropExpandIndex}-${cropExpandUploadId}.png`,
|
||||
);
|
||||
const uploadedCropExpand = await uploadEditorMediaAssetFile(
|
||||
cropExpandFile,
|
||||
@@ -1447,7 +1453,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
'editor',
|
||||
'crop-expand',
|
||||
cropExpandProjectId,
|
||||
`${Date.now()}`,
|
||||
cropExpandUploadId,
|
||||
],
|
||||
entityId: cropExpandProjectId,
|
||||
metadata: {
|
||||
@@ -1480,8 +1486,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
cropExpandObjectKey =
|
||||
cropExpandResource.objectKey ?? uploadedCropExpand.objectKey;
|
||||
cropExpandAssetObjectId =
|
||||
cropExpandResource.assetObjectId ??
|
||||
uploadedCropExpand.assetObjectId;
|
||||
cropExpandResource.assetObjectId ?? uploadedCropExpand.assetObjectId;
|
||||
cropExpandSourceResourceId =
|
||||
cropExpandResource.sourceResourceId ??
|
||||
cropExpandSourceLayer.resourceId;
|
||||
@@ -1574,10 +1579,11 @@ export function useImageCanvasGenerationWorkflow({
|
||||
}
|
||||
: undefined;
|
||||
try {
|
||||
const sourceObjectKey = sourceLayer.objectKey?.trim();
|
||||
const sourceImageSrc = sourceObjectKey
|
||||
? `/${sourceObjectKey.replace(/^\/+/u, '')}`
|
||||
: await resolveEditorImageReferenceDataUrl(sourceLayer.src);
|
||||
const sourceImageSrc = await resolveEditorGenerationMediaReference(
|
||||
sourceLayer,
|
||||
'image',
|
||||
projectId,
|
||||
);
|
||||
const result = await removeImageBackground({
|
||||
sourceImageSrc,
|
||||
projectId,
|
||||
@@ -1672,7 +1678,6 @@ export function useImageCanvasGenerationWorkflow({
|
||||
openPlacedCanvasGenerationDialog,
|
||||
projectId,
|
||||
refreshTaskListForQueuedGeneration,
|
||||
resolveEditorImageReferenceDataUrl,
|
||||
setActiveSidebarPanel,
|
||||
setImageContextMenu,
|
||||
setMetadataLayer,
|
||||
|
||||
Reference in New Issue
Block a user