新抠图算法 (#85)
Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/85 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #85.
This commit is contained in:
@@ -2938,7 +2938,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
phaseDetail: '生成已完成。',
|
||||
warning: '连通域数量不足',
|
||||
warning: '图集已生成,但自动拆分未完成:连通域数量不足',
|
||||
completedAt: '2026-06-21T00:01:00.000Z',
|
||||
updatedAt: '2026-06-21T00:01:00.000Z',
|
||||
updatedAtMicros: 2,
|
||||
|
||||
@@ -1251,9 +1251,7 @@ export function ImageCanvasEditorView({
|
||||
}
|
||||
const warning = tasks.find((task) => task.warning?.trim())?.warning?.trim();
|
||||
if (warning) {
|
||||
showGenerationWarning(
|
||||
`图集已生成,但自动拆分未完成:${warning}`,
|
||||
);
|
||||
showGenerationWarning(warning);
|
||||
}
|
||||
refreshEditorWalletBalance();
|
||||
void loadEditorProject(projectId)
|
||||
|
||||
@@ -220,6 +220,8 @@ describe('ImageCanvasTaskSidebarView', () => {
|
||||
createExternalTask({
|
||||
jobId: 'active-current',
|
||||
startedAt,
|
||||
phaseDetail: '正在处理。',
|
||||
progress: 70,
|
||||
}),
|
||||
createExternalTask({
|
||||
jobId: 'active-other-project',
|
||||
@@ -260,8 +262,8 @@ describe('ImageCanvasTaskSidebarView', () => {
|
||||
|
||||
expect(await screen.findByText('图片画布生成图片')).toBeTruthy();
|
||||
expect(screen.getByText(/发光猫咪主视觉/u)).toBeTruthy();
|
||||
expect(screen.getByText('正在生成第 2/4 段。')).toBeTruthy();
|
||||
expect(screen.queryByText(/35%/u)).toBeNull();
|
||||
expect(screen.getByText('正在处理。')).toBeTruthy();
|
||||
expect(screen.queryByText(/\d{1,3}%/u)).toBeNull();
|
||||
expect(screen.getByText(/已用时 1分/u)).toBeTruthy();
|
||||
expect(screen.queryByText(/总进度/u)).toBeNull();
|
||||
expect(screen.queryByText(/不该显示的外部项目任务/u)).toBeNull();
|
||||
|
||||
@@ -1029,7 +1029,14 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
|
||||
it('submits character redraws through the character image prompt contract', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: '角色换成蓝色披风' }),
|
||||
createGenerated({
|
||||
prompt: '角色换成蓝色披风',
|
||||
warning: {
|
||||
code: 'postprocess-failed-source-preserved',
|
||||
reason:
|
||||
'生成任务成功,后处理失败。',
|
||||
},
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
@@ -1080,6 +1087,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe(
|
||||
'layer-source:idle:角色换成蓝色披风:-',
|
||||
);
|
||||
expect(screen.getByTestId('generation-warning').textContent).toBe(
|
||||
'生成任务成功,后处理失败。',
|
||||
);
|
||||
});
|
||||
|
||||
it('submits video quick edits through the video API with the source video', async () => {
|
||||
@@ -1653,7 +1663,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(uploadEditorMediaAssetFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refreshes the wallet balance after a queued generation reaches a terminal state', async () => {
|
||||
it('refreshes the wallet and shows the warning after a queued character generation completes', async () => {
|
||||
const applyProjectSnapshot = vi.fn();
|
||||
const refreshTaskList = vi.fn();
|
||||
const refreshWalletBalance = vi.fn();
|
||||
@@ -1671,9 +1681,12 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
imageSrc: '/generated-editor-images/project/image.png',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
prompt: '队列生成',
|
||||
prompt: '队列角色生成',
|
||||
}),
|
||||
queueState: createQueueState({
|
||||
warning:
|
||||
'生成任务成功,后处理失败。',
|
||||
}),
|
||||
queueState: createQueueState(),
|
||||
});
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
@@ -1682,9 +1695,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
onQueuedGenerationTask={refreshTaskList}
|
||||
onWalletBalanceMayHaveChanged={refreshWalletBalance}
|
||||
initialDialog={{
|
||||
id: 'dialog-generate',
|
||||
mode: 'generate',
|
||||
prompt: '队列生成',
|
||||
id: 'dialog-character',
|
||||
mode: 'character',
|
||||
prompt: '队列角色生成',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
imageModel: 'gpt-image-2',
|
||||
@@ -1712,6 +1725,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(refreshWalletBalance).toHaveBeenCalledTimes(1);
|
||||
expect(applyProjectSnapshot).toHaveBeenCalledWith(backendProject);
|
||||
expect(getExternalGenerationJobStatusMock).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('generation-warning').textContent).toBe(
|
||||
'生成任务成功,后处理失败。',
|
||||
);
|
||||
expect(screen.getByTestId('layers').textContent).not.toContain(
|
||||
'layer-generated-1',
|
||||
);
|
||||
@@ -1805,7 +1821,8 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
await applyQueuedEditorGenerationProject(
|
||||
{
|
||||
queueState: createQueueState({
|
||||
warning: '连通域数量不足',
|
||||
warning:
|
||||
'生成任务成功,后处理失败。',
|
||||
}),
|
||||
},
|
||||
'editor-project-1',
|
||||
@@ -1816,7 +1833,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
);
|
||||
|
||||
expect(onGenerationWarning).toHaveBeenCalledWith(
|
||||
'图集已生成,但自动拆分未完成:连通域数量不足',
|
||||
'生成任务成功,后处理失败。',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2388,6 +2405,11 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'task-ui-assets',
|
||||
warning: {
|
||||
code: 'postprocess-failed-source-preserved',
|
||||
reason:
|
||||
'生成任务成功,后处理失败。',
|
||||
},
|
||||
});
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
@@ -2414,6 +2436,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
'layer-icon-spritesheet-1',
|
||||
);
|
||||
expect(screen.getByTestId('fit-count').textContent).toBe('1');
|
||||
expect(screen.getByTestId('generation-warning').textContent).toBe(
|
||||
'生成任务成功,后处理失败。',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders selected UI marks into the extraction reference image and sends derived extraction pricing', async () => {
|
||||
@@ -2545,7 +2570,15 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
|
||||
it('keeps character generation dialog data after the result is created', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ width: 768, height: 768 }),
|
||||
createGenerated({
|
||||
width: 768,
|
||||
height: 768,
|
||||
warning: {
|
||||
code: 'postprocess-failed-source-preserved',
|
||||
reason:
|
||||
'生成任务成功,后处理失败。',
|
||||
},
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
@@ -2584,6 +2617,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'character:idle:open:layer-generated-1:placeholder:-',
|
||||
);
|
||||
expect(screen.getByTestId('generation-warning').textContent).toBe(
|
||||
'生成任务成功,后处理失败。',
|
||||
);
|
||||
});
|
||||
|
||||
it('moves character animation panels from generating to completed', async () => {
|
||||
|
||||
@@ -88,6 +88,9 @@ import { renderUiDesignAssetExtractionMarkedImage } from './ImageCanvasUiAssetEx
|
||||
|
||||
type CanvasSize = { width: number; height: number };
|
||||
|
||||
const EDITOR_SPRITESHEET_SLICE_WARNING_PREFIX =
|
||||
'图集已生成,但自动拆分未完成:';
|
||||
|
||||
type CanvasGenerationDialogUpdater = (
|
||||
dialog: CanvasGenerationDialogState,
|
||||
) => CanvasGenerationDialogState | null;
|
||||
@@ -438,7 +441,21 @@ function notifyEditorGenerationWarning(
|
||||
if (!reason) {
|
||||
return;
|
||||
}
|
||||
callback?.(`图集已生成,但自动拆分未完成:${reason}`);
|
||||
callback?.(reason);
|
||||
}
|
||||
|
||||
function resolveEditorGenerationWarningMessage(
|
||||
warning: string | null | undefined,
|
||||
sliceWarning: string | null | undefined,
|
||||
) {
|
||||
const commonReason = warning?.trim();
|
||||
if (commonReason) {
|
||||
return commonReason;
|
||||
}
|
||||
const sliceReason = sliceWarning?.trim();
|
||||
return sliceReason
|
||||
? `${EDITOR_SPRITESHEET_SLICE_WARNING_PREFIX}${sliceReason}`
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async function runEditorGenerationWithWalletRefresh<T>(
|
||||
@@ -1097,7 +1114,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
notifyEditorGenerationWarning(
|
||||
generated.sliceWarning?.reason,
|
||||
resolveEditorGenerationWarningMessage(
|
||||
generated.warning?.reason,
|
||||
generated.sliceWarning?.reason,
|
||||
),
|
||||
onGenerationWarning,
|
||||
);
|
||||
if (
|
||||
@@ -1234,7 +1254,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
);
|
||||
rememberImageModel(submissionPlan.rememberImageModel);
|
||||
notifyEditorGenerationWarning(
|
||||
generated.sliceWarning?.reason,
|
||||
resolveEditorGenerationWarningMessage(
|
||||
generated.warning?.reason,
|
||||
generated.sliceWarning?.reason,
|
||||
),
|
||||
onGenerationWarning,
|
||||
);
|
||||
if (
|
||||
@@ -1590,6 +1613,15 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
const characterWarningCallback = isCharacterRedraw
|
||||
? onGenerationWarning
|
||||
: undefined;
|
||||
if (isCharacterRedraw) {
|
||||
notifyEditorGenerationWarning(
|
||||
generated.warning?.reason,
|
||||
characterWarningCallback,
|
||||
);
|
||||
}
|
||||
if (
|
||||
await applyQueuedEditorGenerationProject(
|
||||
generated,
|
||||
@@ -1597,6 +1629,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
characterWarningCallback,
|
||||
)
|
||||
) {
|
||||
if (panelMode === 'quick-edit') {
|
||||
@@ -1681,6 +1714,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
setQuickEditPanel,
|
||||
updateCanvasGenerationDialogById,
|
||||
viewport,
|
||||
onGenerationWarning,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
]);
|
||||
|
||||
@@ -1999,6 +2033,16 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
const characterWarningCallback =
|
||||
imageGenerationInput.kind === 'character'
|
||||
? onGenerationWarning
|
||||
: undefined;
|
||||
if (imageGenerationInput.kind === 'character') {
|
||||
notifyEditorGenerationWarning(
|
||||
generated.warning?.reason,
|
||||
characterWarningCallback,
|
||||
);
|
||||
}
|
||||
if (
|
||||
await applyQueuedEditorGenerationProject(
|
||||
generated,
|
||||
@@ -2006,6 +2050,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
characterWarningCallback,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -2078,6 +2123,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
setGenerateDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
upsertGeneratedAsset,
|
||||
onGenerationWarning,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1868,21 +1868,19 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
expect(screen.getByTestId('sidebar').textContent).toBe('layers');
|
||||
});
|
||||
|
||||
it('resolves private character image before removing background', async () => {
|
||||
it('queues background removal for private character images', async () => {
|
||||
resolveEditorImageReferenceDataUrlMock.mockResolvedValueOnce(
|
||||
'data:image/png;base64,resolved-character',
|
||||
);
|
||||
removeImageBackgroundMock.mockResolvedValueOnce({
|
||||
imageSrc:
|
||||
'/generated-character-drafts/editor/background-removal/result.png',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/background-removal/result.png',
|
||||
assetObjectId: 'asset-object-background-removal',
|
||||
width: 512,
|
||||
height: 768,
|
||||
taskId: 'background-removal-task',
|
||||
elapsedMs: 1234,
|
||||
provider: 'BiRefNet',
|
||||
queueState: {
|
||||
operationId: 'background-removal-task',
|
||||
status: 'completed',
|
||||
phaseLabel: '已完成',
|
||||
phaseDetail: '已完成',
|
||||
progress: 100,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
});
|
||||
render(
|
||||
<GenerationWorkflowHarness
|
||||
@@ -1912,19 +1910,20 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
sourceResourceId: 'resource-source',
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('task-refresh-key').textContent).toBe('1');
|
||||
});
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-source:源图:resource-source:character:/generated-character-drafts/editor/background-removal/result.png:background-removal-task',
|
||||
'/api/assets/read-url?objectKey=generated-character-drafts/editor/private.png',
|
||||
);
|
||||
expect(screen.getByTestId('layers').textContent).not.toContain(
|
||||
'background-removal-task',
|
||||
);
|
||||
expect(screen.getByTestId('task-refresh-key').textContent).toBe('0');
|
||||
});
|
||||
|
||||
it('opens a generating canvas placeholder when removing background in a project', async () => {
|
||||
let resolveBackgroundRemoval: ((value: unknown) => void) | null = null;
|
||||
removeImageBackgroundMock.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveBackgroundRemoval = resolve;
|
||||
}),
|
||||
() => new Promise(() => {}),
|
||||
);
|
||||
|
||||
render(
|
||||
@@ -1960,136 +1959,6 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
}),
|
||||
);
|
||||
expect(resolveEditorImageReferenceDataUrlMock).not.toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
resolveBackgroundRemoval?.({
|
||||
imageSrc:
|
||||
'/generated-character-drafts/editor/background-removal/project-result.png',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/background-removal/project-result.png',
|
||||
assetObjectId: 'asset-object-background-removal',
|
||||
width: 512,
|
||||
height: 768,
|
||||
taskId: 'background-removal-project-task',
|
||||
elapsedMs: 1234,
|
||||
provider: 'BiRefNet',
|
||||
project: null,
|
||||
});
|
||||
});
|
||||
expect(screen.getByTestId('layers').textContent).not.toContain(
|
||||
'project-result.png',
|
||||
);
|
||||
expect(screen.getByTestId('layers').textContent).not.toContain(
|
||||
'background-removal-project-task',
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves a just-created crop layer when applying a background removal project snapshot', async () => {
|
||||
const applyProjectSnapshot = vi.fn();
|
||||
let resolveBackgroundRemoval: ((value: unknown) => void) | null = null;
|
||||
removeImageBackgroundMock.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveBackgroundRemoval = resolve;
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<GenerationWorkflowHarness
|
||||
projectId="project-1"
|
||||
applyProjectSnapshot={applyProjectSnapshot}
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
id: 'layer-crop-expand-1',
|
||||
resourceId: 'resource-crop-expand',
|
||||
title: '源图 裁扩',
|
||||
src: '/generated-character-drafts/editor/crop-expand/project-1/result.png',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/crop-expand/project-1/result.png',
|
||||
assetObjectId: 'asset-object-crop-expand',
|
||||
sourceType: 'generated',
|
||||
sourceResourceId: 'resource-source',
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '去除背景' }));
|
||||
|
||||
await waitFor(() => expect(removeImageBackgroundMock).toHaveBeenCalled());
|
||||
await act(async () => {
|
||||
resolveBackgroundRemoval?.({
|
||||
imageSrc:
|
||||
'/generated-character-drafts/editor/background-removal/project-result.png',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/background-removal/project-result.png',
|
||||
assetObjectId: 'asset-object-background-removal',
|
||||
width: 512,
|
||||
height: 768,
|
||||
taskId: 'background-removal-project-task',
|
||||
elapsedMs: 1234,
|
||||
provider: 'BiRefNet',
|
||||
project: {
|
||||
projectId: 'project-1',
|
||||
title: '队列项目',
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers: [
|
||||
{
|
||||
layerId: 'layer-background-removal-result',
|
||||
resourceId: 'resource-background-removal',
|
||||
title: '源图 裁扩 去背景',
|
||||
x: 480,
|
||||
y: 140,
|
||||
width: 512,
|
||||
height: 768,
|
||||
originalWidth: 512,
|
||||
originalHeight: 768,
|
||||
zIndex: 9,
|
||||
sourceType: 'generated',
|
||||
},
|
||||
],
|
||||
resources: [
|
||||
{
|
||||
resourceId: 'resource-background-removal',
|
||||
projectId: 'project-1',
|
||||
imageSrc:
|
||||
'/generated-character-drafts/editor/background-removal/project-result.png',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/background-removal/project-result.png',
|
||||
assetObjectId: 'asset-object-background-removal',
|
||||
width: 512,
|
||||
height: 768,
|
||||
sourceType: 'generated',
|
||||
sourceResourceId: 'resource-crop-expand',
|
||||
},
|
||||
],
|
||||
updatedAt: '2026-06-23T08:00:01.000Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const appliedProject = applyProjectSnapshot.mock.calls[0]?.[0];
|
||||
expect(appliedProject?.layers).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
layerId: 'layer-crop-expand-1',
|
||||
resourceId: 'resource-crop-expand',
|
||||
title: '源图 裁扩',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
layerId: 'layer-background-removal-result',
|
||||
resourceId: 'resource-background-removal',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(appliedProject?.resources).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
resourceId: 'resource-crop-expand',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/crop-expand/project-1/result.png',
|
||||
assetObjectId: 'asset-object-crop-expand',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('opens UI design extraction as a mark selection state before submitting', () => {
|
||||
|
||||
@@ -1613,58 +1613,19 @@ export function useImageCanvasGenerationWorkflow({
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
if (
|
||||
await applyQueuedEditorGenerationProject(
|
||||
result,
|
||||
projectId,
|
||||
applyProjectSnapshot,
|
||||
refreshTaskListForQueuedGeneration,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
setGenerationWarning,
|
||||
backgroundRemovalDialogId,
|
||||
applyBackgroundRemovalProjectSnapshot
|
||||
? (project) =>
|
||||
preserveSourceLayerInProjectSnapshot(project, sourceLayer)
|
||||
: undefined,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (result.project && applyBackgroundRemovalProjectSnapshot) {
|
||||
applyBackgroundRemovalProjectSnapshot(result.project);
|
||||
return;
|
||||
}
|
||||
if (backgroundRemovalPlacement?.placeholder && applyProjectSnapshot) {
|
||||
return;
|
||||
}
|
||||
updateSourceLayer(sourceLayer.id, (layer) => ({
|
||||
...layer,
|
||||
resourceId: result.resource?.resourceId ?? layer.resourceId,
|
||||
src: result.imageSrc,
|
||||
width: result.width,
|
||||
height: result.height,
|
||||
originalWidth: result.width,
|
||||
originalHeight: result.height,
|
||||
sourceType: result.sourceType ?? 'generated',
|
||||
provider: result.provider ?? 'BiRefNet',
|
||||
taskId: result.taskId ?? null,
|
||||
objectKey: result.resource?.objectKey ?? result.objectKey ?? null,
|
||||
assetObjectId:
|
||||
result.resource?.assetObjectId ?? result.assetObjectId ?? null,
|
||||
sourceResourceId:
|
||||
result.resource?.sourceResourceId ?? sourceLayer.resourceId,
|
||||
sourceAssetId: null,
|
||||
prompt: result.resource?.prompt ?? layer.prompt,
|
||||
actualPrompt: result.resource?.actualPrompt ?? layer.actualPrompt,
|
||||
model: result.resource?.model ?? layer.model,
|
||||
assetKind:
|
||||
(result.resource?.assetKind as CanvasLayer['assetKind']) ??
|
||||
layer.assetKind,
|
||||
generationInputs:
|
||||
result.resource?.generationInputs ?? layer.generationInputs,
|
||||
generatedAssetSnapshot: result.asset ?? layer.generatedAssetSnapshot,
|
||||
}));
|
||||
setActiveSidebarPanel('layers');
|
||||
await applyQueuedEditorGenerationProject(
|
||||
result,
|
||||
projectId,
|
||||
applyProjectSnapshot,
|
||||
refreshTaskListForQueuedGeneration,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
setGenerationWarning,
|
||||
backgroundRemovalDialogId,
|
||||
applyBackgroundRemovalProjectSnapshot
|
||||
? (project) =>
|
||||
preserveSourceLayerInProjectSnapshot(project, sourceLayer)
|
||||
: undefined,
|
||||
);
|
||||
} catch (error) {
|
||||
if (backgroundRemovalDialogId) {
|
||||
updateCanvasGenerationDialogById(
|
||||
@@ -1689,11 +1650,9 @@ export function useImageCanvasGenerationWorkflow({
|
||||
openPlacedCanvasGenerationDialog,
|
||||
projectId,
|
||||
refreshTaskListForQueuedGeneration,
|
||||
setActiveSidebarPanel,
|
||||
setImageContextMenu,
|
||||
setMetadataLayer,
|
||||
updateCanvasGenerationDialogById,
|
||||
updateSourceLayer,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -743,6 +743,11 @@ describe('editorProjectClient', () => {
|
||||
model: 'gpt-image-2',
|
||||
provider: 'VectorEngine',
|
||||
taskId: 'vector-task-1',
|
||||
warning: {
|
||||
code: 'postprocess-failed-source-preserved',
|
||||
reason:
|
||||
'生成任务成功,后处理失败。',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await generateEditorImage({
|
||||
@@ -750,6 +755,11 @@ describe('editorProjectClient', () => {
|
||||
});
|
||||
|
||||
expect(result.taskId).toBe('vector-task-1');
|
||||
expect(result.warning).toEqual({
|
||||
code: 'postprocess-failed-source-preserved',
|
||||
reason:
|
||||
'生成任务成功,后处理失败。',
|
||||
});
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/editor/images/generations',
|
||||
expect.objectContaining({
|
||||
@@ -1618,18 +1628,17 @@ describe('editorProjectClient', () => {
|
||||
|
||||
it('passes canvas completion context to background removal', async () => {
|
||||
requestJsonMock.mockResolvedValueOnce({
|
||||
imageSrc: 'data:image/png;base64,cutout',
|
||||
width: 512,
|
||||
height: 512,
|
||||
objectKey: 'generated-character-drafts/editor-cutouts/cutout.png',
|
||||
assetObjectId: 'asset-object-cutout',
|
||||
taskId: 'background-removal-1',
|
||||
elapsedMs: 1200,
|
||||
provider: 'BiRefNet',
|
||||
project: null,
|
||||
queueState: {
|
||||
operationId: 'background-removal-1',
|
||||
status: 'queued',
|
||||
phaseLabel: '排队中',
|
||||
phaseDetail: '排队中',
|
||||
progress: 0,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await removeEditorImageBackground({
|
||||
const response = await removeEditorImageBackground({
|
||||
sourceImageSrc: 'data:image/png;base64,source',
|
||||
projectId: 'editor-project-1',
|
||||
targetLayerId: 'layer-source',
|
||||
@@ -1647,6 +1656,8 @@ describe('editorProjectClient', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.queueState.operationId).toBe('background-removal-1');
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/editor/images/background-removals',
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -273,23 +273,12 @@ export type EditorImageGenerationResult = {
|
||||
resource?: EditorProjectResourceSnapshot | null;
|
||||
asset?: EditorAssetSnapshot | null;
|
||||
project?: EditorProjectSnapshot | null;
|
||||
warning?: EditorGenerationWarning | null;
|
||||
queueState?: ExternalGenerationJobStatusRecord | null;
|
||||
};
|
||||
|
||||
export type EditorBackgroundRemovalResult = {
|
||||
imageSrc: string;
|
||||
objectKey?: string | null;
|
||||
assetObjectId?: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
sourceType?: 'generated';
|
||||
taskId?: string | null;
|
||||
elapsedMs?: number;
|
||||
provider?: string;
|
||||
resource?: EditorProjectResourceSnapshot | null;
|
||||
asset?: EditorAssetSnapshot | null;
|
||||
project?: EditorProjectSnapshot | null;
|
||||
queueState?: ExternalGenerationJobStatusRecord | null;
|
||||
queueState: ExternalGenerationJobStatusRecord;
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetIconResult = {
|
||||
@@ -301,11 +290,13 @@ export type EditorIconSpritesheetIconResult = {
|
||||
asset?: EditorAssetSnapshot | null;
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetSliceWarning = {
|
||||
export type EditorGenerationWarning = {
|
||||
code: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export type EditorIconSpritesheetSliceWarning = EditorGenerationWarning;
|
||||
|
||||
export type EditorIconSpritesheetGenerationResult = {
|
||||
spritesheetImageSrc: string;
|
||||
spritesheetWidth: number;
|
||||
@@ -321,6 +312,7 @@ export type EditorIconSpritesheetGenerationResult = {
|
||||
spritesheetResource?: EditorProjectResourceSnapshot | null;
|
||||
spritesheetAsset?: EditorAssetSnapshot | null;
|
||||
project?: EditorProjectSnapshot | null;
|
||||
warning?: EditorGenerationWarning | null;
|
||||
queueState?: ExternalGenerationJobStatusRecord | null;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user