修复图片交付尺寸与提示重触发
统一图片模型的业务交付像素并保留 GPT provider 合法请求尺寸 禁止欠交付图片被放大并返回尺寸降级提示 重置相同 warning 的三秒自动关闭计时 补充前后端回归测试与编辑器契约文档
This commit is contained in:
@@ -233,10 +233,10 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
imageSize: '1K',
|
||||
uiDesignSpecReference: null,
|
||||
placeholder: {
|
||||
x: -64,
|
||||
y: 56,
|
||||
width: 1088,
|
||||
height: 608,
|
||||
x: -32,
|
||||
y: 72,
|
||||
width: 1024,
|
||||
height: 576,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -622,9 +622,9 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
placeholder: {
|
||||
x: 472,
|
||||
y: 140,
|
||||
width: 1376,
|
||||
width: 1365,
|
||||
height: 2048,
|
||||
originalWidth: 1376,
|
||||
originalWidth: 1365,
|
||||
originalHeight: 2048,
|
||||
},
|
||||
});
|
||||
@@ -662,9 +662,9 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
placeholder: {
|
||||
x: 472,
|
||||
y: 140,
|
||||
width: 1376,
|
||||
width: 1365,
|
||||
height: 2048,
|
||||
originalWidth: 1376,
|
||||
originalWidth: 1365,
|
||||
originalHeight: 2048,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -108,18 +108,18 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
[IMAGE_MODEL_NANOBANANA2, '2K', '2:3', 1365, 2048],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '1:1', 1024, 1024],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '4:3', 1024, 768],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '3:2', 1024, 688],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '2:3', 688, 1024],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '9:16', 608, 1088],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '16:9', 1088, 608],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '3:2', 1024, 683],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '2:3', 683, 1024],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '9:16', 576, 1024],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '1K', '16:9', 1024, 576],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '1:1', 2048, 2048],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '4:3', 2048, 1536],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '3:2', 2048, 1376],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '2:3', 1376, 2048],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '3:2', 2048, 1365],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '2:3', 1365, 2048],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '9:16', 1152, 2048],
|
||||
[IMAGE_MODEL_GPT_IMAGE_2, '2K', '16:9', 2048, 1152],
|
||||
] as const)(
|
||||
'%s 的 %s %s 占位尺寸与 provider 直接请求尺寸一致',
|
||||
'%s 的 %s %s 占位尺寸与统一业务交付尺寸一致',
|
||||
(model, imageSize, aspectRatio, width, height) => {
|
||||
expect(
|
||||
resolveEditorImageGenerationPixelSize({
|
||||
|
||||
@@ -173,38 +173,8 @@ function resolveImageFrameSizeFromRatio({
|
||||
};
|
||||
}
|
||||
|
||||
function alignImageDimensionUp(value: number, alignment = 16) {
|
||||
return Math.ceil(value / alignment) * alignment;
|
||||
}
|
||||
|
||||
function resolveGptImage2FrameSize({
|
||||
aspectRatio,
|
||||
imageSize,
|
||||
}: {
|
||||
aspectRatio: string | null | undefined;
|
||||
imageSize: string | null | undefined;
|
||||
}): EditorImageFrameSize {
|
||||
const requested = resolveImageFrameSizeFromRatio({ aspectRatio, imageSize });
|
||||
let width = requested.width;
|
||||
let height = requested.height;
|
||||
const pixels = width * height;
|
||||
|
||||
// 中文注释:gpt-image-2 要求总像素不少于 655360,且宽高均为 16 的倍数。
|
||||
// 这里只把用户选择的 K 档和比例转换为 provider 可直接生成的真实尺寸,不做生成后放大。
|
||||
if (pixels < 655_360) {
|
||||
const scale = Math.sqrt(655_360 / pixels);
|
||||
width *= scale;
|
||||
height *= scale;
|
||||
}
|
||||
|
||||
return {
|
||||
width: alignImageDimensionUp(width),
|
||||
height: alignImageDimensionUp(height),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveEditorImageGenerationPixelSize({
|
||||
model,
|
||||
model: _model,
|
||||
aspectRatio,
|
||||
imageSize,
|
||||
}: {
|
||||
@@ -212,9 +182,6 @@ export function resolveEditorImageGenerationPixelSize({
|
||||
aspectRatio: string | null | undefined;
|
||||
imageSize: string | null | undefined;
|
||||
}): EditorImageFrameSize {
|
||||
if (model?.trim() === IMAGE_MODEL_GPT_IMAGE_2) {
|
||||
return resolveGptImage2FrameSize({ aspectRatio, imageSize });
|
||||
}
|
||||
return resolveImageFrameSizeFromRatio({ aspectRatio, imageSize });
|
||||
}
|
||||
|
||||
|
||||
@@ -671,6 +671,42 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows dimension restore warnings after non-character quick edits', async () => {
|
||||
editEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({
|
||||
prompt: '改成雨天',
|
||||
width: 1024,
|
||||
height: 576,
|
||||
warning: {
|
||||
code: 'dimension-restore-fallback',
|
||||
reason: 'provider 回图小于目标尺寸,已保留实际回图。',
|
||||
},
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialQuickEditPanel={{
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: '改成雨天',
|
||||
size: '2048x1152',
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
model: 'gpt-image-2',
|
||||
status: 'idle',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('generation-warning').textContent).toBe(
|
||||
'provider 回图小于目标尺寸,已保留实际回图。',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects direct quick edit submission for icon assets', async () => {
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
|
||||
@@ -1643,15 +1643,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
const characterWarningCallback = isCharacterRedraw
|
||||
? onGenerationWarning
|
||||
: undefined;
|
||||
if (isCharacterRedraw) {
|
||||
notifyEditorGenerationWarning(
|
||||
generated.warning?.reason,
|
||||
characterWarningCallback,
|
||||
);
|
||||
}
|
||||
notifyEditorGenerationWarning(
|
||||
generated.warning?.reason,
|
||||
onGenerationWarning,
|
||||
);
|
||||
if (
|
||||
await applyQueuedEditorGenerationProject(
|
||||
generated,
|
||||
@@ -1659,7 +1654,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
characterWarningCallback,
|
||||
onGenerationWarning,
|
||||
)
|
||||
) {
|
||||
if (panelMode === 'quick-edit') {
|
||||
@@ -2066,16 +2061,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
const characterWarningCallback =
|
||||
imageGenerationInput.kind === 'character'
|
||||
? onGenerationWarning
|
||||
: undefined;
|
||||
if (imageGenerationInput.kind === 'character') {
|
||||
notifyEditorGenerationWarning(
|
||||
generated.warning?.reason,
|
||||
characterWarningCallback,
|
||||
);
|
||||
}
|
||||
notifyEditorGenerationWarning(
|
||||
generated.warning?.reason,
|
||||
onGenerationWarning,
|
||||
);
|
||||
if (
|
||||
await applyQueuedEditorGenerationProject(
|
||||
generated,
|
||||
@@ -2083,7 +2072,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
characterWarningCallback,
|
||||
onGenerationWarning,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
|
||||
@@ -349,6 +349,7 @@ export function useImageCanvasGenerationSurface({
|
||||
<>
|
||||
{generationWorkflow.generationWarning ? (
|
||||
<PlatformRuntimeStatusToast
|
||||
key={generationWorkflow.generationWarningVersion}
|
||||
tone="warning"
|
||||
surface="solid"
|
||||
size="sm"
|
||||
|
||||
@@ -1447,6 +1447,37 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('restarts the dismissal timer when the same warning is shown again', () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开角色生成' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择非规范角色图' }));
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(2_999);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择非规范角色图' }));
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('reference-pick-warning').textContent).toBe(
|
||||
'选择的图片不是角色规范图,请选择生成规范里的角色规范。',
|
||||
);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(2_999);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('reference-pick-warning').textContent).toBe('-');
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('restores the last picked character spec from local cache after remounting the editor', () => {
|
||||
const firstRender = render(<GenerationWorkflowHarness />);
|
||||
|
||||
|
||||
@@ -720,6 +720,11 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const [generationWarning, setGenerationWarning] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [generationWarningVersion, setGenerationWarningVersion] = useState(0);
|
||||
const showGenerationWarning = useCallback((warning: string) => {
|
||||
setGenerationWarning(warning);
|
||||
setGenerationWarningVersion((version) => version + 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!generationWarning) {
|
||||
@@ -733,7 +738,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
return () => {
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [generationWarning]);
|
||||
}, [generationWarning, generationWarningVersion]);
|
||||
const [cropExpandPanel, setCropExpandPanel] =
|
||||
useState<CropExpandPanelState | null>(null);
|
||||
const [uiAssetExtractionState, setUiAssetExtractionState] =
|
||||
@@ -1335,7 +1340,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
return;
|
||||
}
|
||||
if (sourceLayer.assetKind === 'character-animation') {
|
||||
setGenerationWarning('未找到角色动作关联的原角色图层,无法改造。');
|
||||
showGenerationWarning('未找到角色动作关联的原角色图层,无法改造。');
|
||||
return;
|
||||
}
|
||||
const redrawPanel = createRedrawPanelDraft(sourceLayer, {
|
||||
@@ -1375,6 +1380,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setActiveTool,
|
||||
setImageContextMenu,
|
||||
setMetadataLayer,
|
||||
showGenerationWarning,
|
||||
canvasGenerationDialogs,
|
||||
canvasSize,
|
||||
layers,
|
||||
@@ -1703,7 +1709,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
}),
|
||||
refreshTaskListForQueuedGeneration,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
setGenerationWarning,
|
||||
showGenerationWarning,
|
||||
backgroundRemovalDialogId,
|
||||
applyBackgroundRemovalProjectSnapshot
|
||||
? (project) =>
|
||||
@@ -1736,6 +1742,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
refreshTaskListForQueuedGeneration,
|
||||
setImageContextMenu,
|
||||
setMetadataLayer,
|
||||
showGenerationWarning,
|
||||
updateCanvasGenerationDialogById,
|
||||
],
|
||||
);
|
||||
@@ -1819,7 +1826,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const pickCharacterSpecFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
if (!isCharacterSpecReferenceLayer(layer)) {
|
||||
setGenerationWarning(INVALID_CHARACTER_SPEC_WARNING);
|
||||
showGenerationWarning(INVALID_CHARACTER_SPEC_WARNING);
|
||||
return;
|
||||
}
|
||||
setGenerationWarning(null);
|
||||
@@ -1838,7 +1845,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setIsCharacterSpecMenuOpen(false);
|
||||
setImageContextMenu(null);
|
||||
},
|
||||
[setGenerateDialog, setImageContextMenu],
|
||||
[setGenerateDialog, setImageContextMenu, showGenerationWarning],
|
||||
);
|
||||
|
||||
const pickGenerationReferenceFromLayer = useCallback(
|
||||
@@ -1879,7 +1886,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const pickIconSpecFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
if (!isIconSpecReferenceLayer(layer)) {
|
||||
setGenerationWarning(INVALID_ICON_SPEC_WARNING);
|
||||
showGenerationWarning(INVALID_ICON_SPEC_WARNING);
|
||||
return;
|
||||
}
|
||||
setGenerationWarning(null);
|
||||
@@ -1895,13 +1902,18 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setIsIconSpecMenuOpen(false);
|
||||
setImageContextMenu(null);
|
||||
},
|
||||
[generateDialog, setGenerateDialog, setImageContextMenu],
|
||||
[
|
||||
generateDialog,
|
||||
setGenerateDialog,
|
||||
setImageContextMenu,
|
||||
showGenerationWarning,
|
||||
],
|
||||
);
|
||||
|
||||
const pickUiDesignSpecFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
if (!isIconSpecReferenceLayer(layer)) {
|
||||
setGenerationWarning(INVALID_ICON_SPEC_WARNING);
|
||||
showGenerationWarning(INVALID_ICON_SPEC_WARNING);
|
||||
return;
|
||||
}
|
||||
setGenerationWarning(null);
|
||||
@@ -1920,7 +1932,12 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setIsUiDesignSpecMenuOpen(false);
|
||||
setImageContextMenu(null);
|
||||
},
|
||||
[generateDialog, setGenerateDialog, setImageContextMenu],
|
||||
[
|
||||
generateDialog,
|
||||
setGenerateDialog,
|
||||
setImageContextMenu,
|
||||
showGenerationWarning,
|
||||
],
|
||||
);
|
||||
|
||||
const pickPublicationReferenceFromLayer = useCallback(
|
||||
@@ -1986,7 +2003,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask: refreshTaskListForQueuedGeneration,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
onGenerationWarning: setGenerationWarning,
|
||||
onGenerationWarning: showGenerationWarning,
|
||||
});
|
||||
|
||||
const setEffectiveCharacterAnimationPanel: Dispatch<
|
||||
@@ -2554,7 +2571,8 @@ export function useImageCanvasGenerationWorkflow({
|
||||
isPickingPublicationReferenceFromCanvas,
|
||||
setIsPickingPublicationReferenceFromCanvas,
|
||||
generationWarning,
|
||||
showGenerationWarning: setGenerationWarning,
|
||||
generationWarningVersion,
|
||||
showGenerationWarning,
|
||||
clearGenerationWarning: () => setGenerationWarning(null),
|
||||
openGenerateDialog,
|
||||
openSpecDialog,
|
||||
@@ -2649,6 +2667,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
isSpecMenuOpen,
|
||||
isPickingPublicationReferenceFromCanvas,
|
||||
generationWarning,
|
||||
generationWarningVersion,
|
||||
openBackgroundMusicGenerationDialog,
|
||||
openCharacterAnimationPanel,
|
||||
openCharacterGenerationDialog,
|
||||
@@ -2686,6 +2705,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
updateIconDescriptionsText,
|
||||
updateSpecFormValue,
|
||||
rememberImageModel,
|
||||
showGenerationWarning,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user