收紧序列帧去背景来源与绿幕门禁
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Backend tests (pull_request) Failing after 13s
Project CI / Frontend tests (pull_request) Successful in 2m38s
Project CI / Native shell tests (pull_request) Failing after 11m22s

验证去背景每帧正式资产对象的用户归属、对象路径和来源任务
仅允许编辑器生成的纯绿幕角色动画执行算法去背景
补齐去背景生成 action 白名单但保持不可改造
更新工具栏门禁、回归测试和Spine序列帧技术方案
This commit is contained in:
2026-08-15 14:50:18 +08:00
parent 2c85ea0b83
commit 101fd952d5
7 changed files with 336 additions and 30 deletions
@@ -26,6 +26,7 @@ export const CANVAS_GENERATION_ACTIONS = [
'audio.background-music.generate',
'character-animation.generate',
'character-animation.convert',
'character-animation.remove-background',
'image.edit',
'ui-design.extract-assets',
'image.perfect-pixel',
@@ -5,6 +5,10 @@ import type {
CanvasGenerationDialogState,
CanvasLayer,
} from './ImageCanvasEditorTypes';
import {
hydrateCanvasGenerationInputsResult,
isRemixableCanvasGenerationAction,
} from './ImageCanvasGenerationInputsModel';
import {
applyEditorGenerationPricingConfig,
BACKGROUND_MUSIC_MODEL_SUNO,
@@ -31,6 +35,7 @@ import {
calculateEditorUiDesignPrice,
calculateEditorVideoPrice,
canOpenRedrawPanel,
canRemoveCharacterAnimationBackground,
CANVAS_GENERATION_PARAMETER_FALLBACK_WARNING,
CHARACTER_ANIMATION_MODEL,
createCanvasLayerReference,
@@ -65,6 +70,62 @@ import {
} from './ImageCanvasGenerationModel';
describe('ImageCanvasGenerationModel', () => {
it('allows sequence background removal only for green generated animations', () => {
const layer = {
assetKind: 'character-animation',
generationInputs: {
version: 2 as const,
action: 'character-animation.generate' as const,
fields: [
{ id: 'backgroundColor', title: '背景颜色', value: 'green' as const },
],
references: [],
},
} as unknown as CanvasLayer;
expect(canRemoveCharacterAnimationBackground(layer)).toBe(true);
expect(
canRemoveCharacterAnimationBackground({
...layer,
generationInputs: {
...layer.generationInputs!,
action: 'character-animation.convert',
},
}),
).toBe(false);
expect(
canRemoveCharacterAnimationBackground({
...layer,
generationInputs: {
...layer.generationInputs!,
fields: [{ id: 'backgroundColor', title: '背景颜色', value: 'blue' }],
},
}),
).toBe(false);
expect(
canRemoveCharacterAnimationBackground({
...layer,
generationInputs: undefined,
}),
).toBe(false);
});
it('hydrates remove-background as a known but non-remixable action', () => {
const result = hydrateCanvasGenerationInputsResult({
version: 2,
action: 'character-animation.remove-background',
fields: [],
references: [],
});
expect(result.state).toBe('valid');
expect(result.inputs?.action).toBe('character-animation.remove-background');
expect(
isRemixableCanvasGenerationAction(
'character-animation.remove-background',
),
).toBe(false);
});
it('uses a character animation preview video when a video reference is requested', () => {
const reference = createCanvasLayerReference(
{
@@ -1042,6 +1042,19 @@ export function buildCharacterAnimationGenerationInputs(
};
}
export function canRemoveCharacterAnimationBackground(layer: CanvasLayer) {
if (
layer.assetKind !== 'character-animation' ||
layer.generationInputs?.version !== 2 ||
layer.generationInputs.action !== 'character-animation.generate'
) {
return false;
}
return layer.generationInputs.fields.some(
(field) => field.id === 'backgroundColor' && field.value === 'green',
);
}
export function canSplitCharacterAnimationFrames(layer: CanvasLayer) {
const resourceId = resolveRegisteredProjectResourceId(layer.resourceId);
return (
@@ -460,7 +460,10 @@ describe('ImageCanvasSelectedLayerToolbarView', () => {
generationInputs: {
version: 2,
action: 'character-animation.generate',
fields: [{ id: 'prompt', title: '动作描述', value: '挥手' }],
fields: [
{ id: 'prompt', title: '动作描述', value: '挥手' },
{ id: 'backgroundColor', title: '背景颜色', value: 'green' },
],
references: [],
},
}),
@@ -489,6 +492,40 @@ describe('ImageCanvasSelectedLayerToolbarView', () => {
);
});
it('hides sequence background removal unless the authoritative background is green', () => {
const whiteLayer = createLayer({
mediaType: 'image-sequence',
assetKind: 'character-animation',
generationInputs: {
version: 2,
action: 'character-animation.generate',
fields: [
{ id: 'prompt', title: '动作描述', value: '挥手' },
{ id: 'backgroundColor', title: '背景颜色', value: 'white' },
],
references: [],
},
});
renderSelectedToolbar({ selectedLayer: whiteLayer });
expect(screen.queryByRole('button', { name: '去背景' })).toBeNull();
cleanup();
const convertedLayer = createLayer({
mediaType: 'image-sequence',
assetKind: 'character-animation',
generationInputs: {
version: 2,
action: 'character-animation.convert',
fields: [{ id: 'backgroundColor', title: '背景颜色', value: 'green' }],
references: [],
},
});
renderSelectedToolbar({ selectedLayer: convertedLayer });
expect(screen.queryByRole('button', { name: '去背景' })).toBeNull();
});
it('disables frame splitting while a character animation resource is pending registration', () => {
const props = renderSelectedToolbar({
selectedLayer: createLayer({
@@ -16,6 +16,7 @@ import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import {
canOpenRedrawPanel,
canRemoveCharacterAnimationBackground,
canSplitCharacterAnimationFrames,
isQuickEditSupportedLayer,
} from './ImageCanvasGenerationModel';
@@ -67,6 +68,8 @@ export function ImageCanvasSelectedLayerToolbarView({
return null;
}
const canRedraw = canOpenRedrawPanel(selectedLayer);
const canRemoveCharacterAnimation =
canRemoveCharacterAnimationBackground(selectedLayer);
const isCharacterAnimationResourcePending =
selectedLayer.assetKind === 'character-animation' &&
!canSplitCharacterAnimationFrames(selectedLayer);
@@ -281,35 +284,39 @@ export function ImageCanvasSelectedLayerToolbarView({
) : null}
{selectedLayer.assetKind === 'character-animation' ? (
<>
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label={
isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'
}
title={
isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'
}
icon={
isRemovingCharacterAnimationBackground ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<ImageOff className="h-4 w-4" />
)
}
disabled={
isCharacterAnimationResourcePending ||
isRemovingCharacterAnimationBackground
}
aria-busy={
isCharacterAnimationResourcePending ||
isRemovingCharacterAnimationBackground
}
onClick={() => onRemoveCharacterAnimationBackground(selectedLayer)}
>
<span>
{isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'}
</span>
</CanvasChromeButton>
{canRemoveCharacterAnimation ? (
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label={
isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'
}
title={
isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'
}
icon={
isRemovingCharacterAnimationBackground ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<ImageOff className="h-4 w-4" />
)
}
disabled={
isCharacterAnimationResourcePending ||
isRemovingCharacterAnimationBackground
}
aria-busy={
isCharacterAnimationResourcePending ||
isRemovingCharacterAnimationBackground
}
onClick={() =>
onRemoveCharacterAnimationBackground(selectedLayer)
}
>
<span>
{isRemovingCharacterAnimationBackground ? '去背景中' : '去背景'}
</span>
</CanvasChromeButton>
) : null}
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label={characterAnimationSplitLabel}